Flutter 网络请求深度解析

@TOC

一、核心网络请求库对比

1. 常用库特性对比

在 Flutter 开发中,选择一个合适的网络请求库是构建稳定应用的第一步。不同的项目规模和团队背景需要不同的技术栈支持。httpdioretrofitchopper 是目前最主流的几种选择。

  • http:由 Flutter 官方团队维护,轻量、简洁,适合初学者或对网络功能要求不高的小型项目。它没有内置拦截器、序列化、超时等高级功能,所有逻辑都需要手动实现。
  • dio:功能极为强大,支持拦截器、请求/响应拦截、文件上传下载、超时配置、自动解析、取消请求等,是中大型项目的首选。其 API 设计清晰,扩展性强,社区生态成熟。
  • retrofit / chopper:借鉴了 Android 平台的 Retrofit 框架思想,采用注解方式定义接口,代码结构优雅,适合有 Android 背景的开发者。但它们依赖代码生成器,在灵活性和调试上略逊于 dio,且生态相对较小。

总结建议

  • dio 是最全面、功能最强大的选择,适合中大型项目。
  • http 轻量简单,适合小型项目或学习使用。
  • retrofitchopper 提供类似 Retrofit 的注解风格,适合习惯 Android 开发的团队,但生态和灵活性略逊于 dio。

二、Dio 深度使用指南

1. 基础配置与初始化

dart 复制代码
// dio_client.dart
class DioClient {
  static final DioClient _instance = DioClient._internal();
  factory DioClient() => _instance;
  DioClient._internal() {
    _init();
  }

  late Dio dio;

  void _init() {
    dio = Dio(
      BaseOptions(
        baseUrl: 'https://api.example.com',
        connectTimeout: const Duration(seconds: 30),
        receiveTimeout: const Duration(seconds: 30),
        sendTimeout: const Duration(seconds: 30),
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      ),
    );

    // 添加拦截器
    dio.interceptors.addAll([
      _getLogInterceptor(),
      _getAuthInterceptor(),
      _getErrorInterceptor(),
      _getCacheInterceptor(),
    ]);
  }

  Interceptor _getLogInterceptor() {
    return LogInterceptor(
      request: true,
      requestBody: true,
      responseBody: true,
      responseHeader: false,
      error: true,
      logPrint: (log) => debugPrint('Dio: $log'),
    );
  }

  Interceptor _getAuthInterceptor() {
    return InterceptorsWrapper(
      onRequest: (options, handler) async {
        // 添加认证token
        final token = await _getAuthToken();
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        return handler.next(options);
      },
      onResponse: (response, handler) {
        // 统一处理响应
        return handler.next(response);
      },
      onError: (DioException error, handler) async {
        // Token过期处理
        if (error.response?.statusCode == 401) {
          final newToken = await _refreshToken();
          if (newToken != null) {
            // 重新发起请求
            final request = error.requestOptions;
            request.headers['Authorization'] = 'Bearer $newToken';
            return handler.resolve(await dio.fetch(request));
          }
        }
        return handler.next(error);
      },
    );
  }

  Interceptor _getErrorInterceptor() {
    return InterceptorsWrapper(
      onError: (DioException error, handler) {
        // 统一错误处理
        final networkError = _handleDioError(error);
        return handler.reject(networkError);
      },
    );
  }

  Interceptor _getCacheInterceptor() {
    return InterceptorsWrapper(
      onRequest: (options, handler) async {
        // 缓存处理
        if (options.extra['cache'] == true) {
          final cachedResponse = await _getCachedResponse(options);
          if (cachedResponse != null) {
            return handler.resolve(cachedResponse);
          }
        }
        return handler.next(options);
      },
      onResponse: (response, handler) async {
        // 缓存响应
        if (response.requestOptions.extra['cache'] == true) {
          await _cacheResponse(response);
        }
        return handler.next(response);
      },
    );
  }

  Future<String?> _getAuthToken() async {
    // 从本地存储获取token
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString('auth_token');
  }

  Future<String?> _refreshToken() async {
    // 刷新token逻辑
    try {
      final response = await dio.post('/refresh-token');
      final newToken = response.data['token'];
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('auth_token', newToken);
      return newToken;
    } catch (e) {
      return null;
    }
  }

  DioException _handleDioError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return DioException(
          requestOptions: error.requestOptions,
          error: '网络连接超时,请检查网络设置',
        );
      case DioExceptionType.badResponse:
        return _handleBadResponse(error);
      case DioExceptionType.cancel:
        return DioException(
          requestOptions: error.requestOptions,
          error: '请求已取消',
        );
      case DioExceptionType.unknown:
        return DioException(
          requestOptions: error.requestOptions,
          error: '网络连接失败,请检查网络设置',
        );
      default:
        return error;
    }
  }

  DioException _handleBadResponse(DioException error) {
    final statusCode = error.response?.statusCode;
    final message = error.response?.data?['message'] ?? '请求失败';
    
    String errorMessage;
    switch (statusCode) {
      case 400:
        errorMessage = '请求参数错误';
        break;
      case 401:
        errorMessage = '未授权,请重新登录';
        break;
      case 403:
        errorMessage = '访问被拒绝';
        break;
      case 404:
        errorMessage = '请求的资源不存在';
        break;
      case 500:
        errorMessage = '服务器内部错误';
        break;
      case 502:
        errorMessage = '网关错误';
        break;
      case 503:
        errorMessage = '服务不可用';
        break;
      default:
        errorMessage = message;
    }
    
    return DioException(
      requestOptions: error.requestOptions,
      error: errorMessage,
      response: error.response,
    );
  }

  Future<Response?> _getCachedResponse(RequestOptions options) async {
    // 实现缓存逻辑
    return null;
  }

  Future<void> _cacheResponse(Response response) async {
    // 实现缓存存储逻辑
  }
}

详细说明:Dio 客户端的单例模式与拦截器体系

上述代码实现了一个全局唯一的 DioClient,采用 单例模式(Singleton Pattern),确保整个应用只存在一个 Dio 实例,避免重复创建和资源浪费。

BaseOptions 配置了基础请求参数,包括:

  • baseUrl:所有请求的前缀,减少重复输入。
  • timeout:连接、发送、接收超时时间,防止请求无限等待。
  • headers:默认请求头,如内容类型和接受格式。

拦截器(Interceptors) 是 Dio 的核心特性之一,允许你在请求/响应生命周期中插入自定义逻辑:

  • 日志拦截器:打印请求和响应内容,便于调试。
  • 鉴权拦截器 :在每个请求头中自动添加 Authorization,并处理 401 错误时的 token 刷新与重试机制(即"自动续签")。
  • 错误拦截器:将 Dio 的原生异常转换为更友好的用户提示信息。
  • 缓存拦截器:根据请求配置决定是否读取或写入缓存,提升用户体验。

该设计实现了关注点分离,让网络层具备日志、安全、容错、性能优化等企业级能力。

三、高级请求封装

1. API 服务类封装

dart 复制代码
// api_service.dart
class ApiService {
  final DioClient _dioClient;

  ApiService(this._dioClient);

  // GET 请求封装
  Future<ApiResponse<T>> get<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    bool cache = false,
    T Function(dynamic)? fromJson,
  }) async {
    try {
      final response = await _dioClient.dio.get(
        path,
        queryParameters: queryParameters,
        options: Options(extra: {'cache': cache}),
      );
      
      return _handleResponse<T>(response, fromJson);
    } on DioException catch (e) {
      return ApiResponse.error(message: e.error?.toString() ?? '网络请求失败');
    } catch (e) {
      return ApiResponse.error(message: '未知错误: $e');
    }
  }

  // POST 请求封装
  Future<ApiResponse<T>> post<T>(
    String path, {
    dynamic data,
    T Function(dynamic)? fromJson,
  }) async {
    try {
      final response = await _dioClient.dio.post(
        path,
        data: data,
      );
      
      return _handleResponse<T>(response, fromJson);
    } on DioException catch (e) {
      return ApiResponse.error(message: e.error?.toString() ?? '网络请求失败');
    } catch (e) {
      return ApiResponse.error(message: '未知错误: $e');
    }
  }

  // 文件上传
  Future<ApiResponse<T>> upload<T>(
    String path,
    String filePath, {
    T Function(dynamic)? fromJson,
    ProgressCallback? onSendProgress,
  }) async {
    try {
      final formData = FormData.fromMap({
        'file': await MultipartFile.fromFile(filePath),
      });

      final response = await _dioClient.dio.post(
        path,
        data: formData,
        onSendProgress: onSendProgress,
      );
      
      return _handleResponse<T>(response, fromJson);
    } on DioException catch (e) {
      return ApiResponse.error(message: e.error?.toString() ?? '文件上传失败');
    } catch (e) {
      return ApiResponse.error(message: '未知错误: $e');
    }
  }

  // 多文件上传
  Future<ApiResponse<T>> uploadMultiple<T>(
    String path,
    List<String> filePaths, {
    T Function(dynamic)? fromJson,
    ProgressCallback? onSendProgress,
  }) async {
    try {
      final formData = FormData();

      for (final filePath in filePaths) {
        formData.files.add(MapEntry(
          'files',
          await MultipartFile.fromFile(filePath),
        ));
      }

      final response = await _dioClient.dio.post(
        path,
        data: formData,
        onSendProgress: onSendProgress,
      );
      
      return _handleResponse<T>(response, fromJson);
    } on DioException catch (e) {
      return ApiResponse.error(message: e.error?.toString() ?? '文件上传失败');
    } catch (e) {
      return ApiResponse.error(message: '未知错误: $e');
    }
  }

  // 文件下载
  Future<ApiResponse<String>> download(
    String url,
    String savePath, {
    ProgressCallback? onReceiveProgress,
  }) async {
    try {
      final response = await _dioClient.dio.download(
        url,
        savePath,
        onReceiveProgress: onReceiveProgress,
      );
      
      if (response.statusCode == 200) {
        return ApiResponse.success(data: savePath);
      } else {
        return ApiResponse.error(message: '下载失败');
      }
    } on DioException catch (e) {
      return ApiResponse.error(message: e.error?.toString() ?? '下载失败');
    } catch (e) {
      return ApiResponse.error(message: '未知错误: $e');
    }
  }

  // 取消请求
  CancelToken createCancelToken() => CancelToken();

  ApiResponse<T> _handleResponse<T>(Response response, T Function(dynamic)? fromJson) {
    if (response.statusCode == 200) {
      try {
        final data = response.data;
        
        if (fromJson != null) {
          final parsedData = fromJson(data);
          return ApiResponse.success(data: parsedData);
        } else {
          return ApiResponse.success(data: data as T);
        }
      } catch (e) {
        return ApiResponse.error(message: '数据解析失败: $e');
      }
    } else {
      return ApiResponse.error(
        message: '请求失败: ${response.statusCode}',
        code: response.statusCode,
      );
    }
  }
}

详细说明:统一 API 服务层的设计思想

ApiService 是对 Dio 的进一步封装,屏蔽底层细节,提供更高层次的 API 接口。

  • 泛型支持 :通过 <T> 支持任意数据类型的返回,结合 fromJson 回调完成 JSON 到模型的转换。
  • 统一错误处理 :捕获 DioException 并封装为 ApiResponse.error,避免在 UI 层处理原始异常。
  • 文件操作支持:封装单/多文件上传与下载,支持进度监听,提升用户体验。
  • 响应处理抽象_handleResponse 方法统一判断状态码、解析数据、处理异常,确保所有请求遵循相同逻辑。
  • 取消请求能力 :暴露 CancelToken,可在页面销毁或用户取消时中断请求,防止内存泄漏。

这种封装方式使得业务代码只需关心"调什么接口",而不必重复编写 try-catch、解析、错误提示等模板代码。

2. 统一响应模型

dart 复制代码
// 统一响应模型
class ApiResponse<T> {
  final T? data;
  final String? message;
  final int? code;
  final bool success;

  ApiResponse({
    this.data,
    this.message,
    this.code,
    required this.success,
  });

  factory ApiResponse.success({T? data, String? message}) {
    return ApiResponse(
      data: data,
      message: message,
      success: true,
    );
  }

  factory ApiResponse.error({String? message, int? code}) {
    return ApiResponse(
      message: message,
      code: code,
      success: false,
    );
  }

  bool get hasData => data != null;
}

详细说明:标准化响应结构的价值

ApiResponse<T> 是一个泛型容器类,用于包装所有网络请求的结果。

它具有以下优势:

  • 结构统一:无论成功还是失败,返回值都符合同一结构,便于状态管理。
  • 类型安全 :通过泛型 T 明确指定数据类型,避免类型转换错误。
  • 语义清晰success 字段明确标识请求结果,hasData 快速判断是否有有效数据。
  • 便于 UI 显示message 可直接用于 Toast 或错误提示框。

该模型是连接网络层与 UI 层的桥梁,极大提升了代码的可读性和健壮性。

四、状态管理集成

1. 基于 Riverpod 的网络状态管理

dart 复制代码
// network_providers.dart
final dioClientProvider = Provider<DioClient>((ref) {
  return DioClient();
});

final apiServiceProvider = Provider<ApiService>((ref) {
  final dioClient = ref.watch(dioClientProvider);
  return ApiService(dioClient);
});

// 用户相关API Provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
  final apiService = ref.watch(apiServiceProvider);
  return UserRepository(apiService);
});

// 用户列表状态管理
final usersProvider = StateNotifierProvider<UsersNotifier, UsersState>((ref) {
  final userRepository = ref.watch(userRepositoryProvider);
  return UsersNotifier(userRepository);
});

详细说明:依赖注入与 Riverpod 的优雅结合

Riverpod 提供了强大的依赖注入机制,上述代码使用 ProviderStateNotifierProvider 实现了分层解耦:

  • dioClientProvider:提供全局 Dio 实例。
  • apiServiceProvider:注入 Dio,创建 ApiService。
  • userRepositoryProvider:注入 ApiService,创建 UserRepository。
  • usersProvider:注入 Repository,管理用户列表状态。

这种方式实现了 控制反转(IoC),组件之间不直接依赖具体实现,而是通过 Provider 获取依赖,便于替换、测试和复用。

2. 状态定义

dart 复制代码
class UsersState {
  final List<User> users;
  final bool isLoading;
  final String? error;
  final bool hasReachedMax;

  const UsersState({
    this.users = const [],
    this.isLoading = false,
    this.error,
    this.hasReachedMax = false,
  });

  UsersState copyWith({
    List<User>? users,
    bool? isLoading,
    String? error,
    bool? hasReachedMax,
  }) {
    return UsersState(
      users: users ?? this.users,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
      hasReachedMax: hasReachedMax ?? this.hasReachedMax,
    );
  }
}

详细说明:不可变状态(Immutable State)的重要性

UsersState 是一个典型的不可变状态类,所有字段都是 final,通过 copyWith 方法创建新实例。

优点包括:

  • 避免副作用:状态不会被意外修改。
  • 易于调试:每次状态变化都产生新对象,便于追踪。
  • 兼容 Riverpod:StateNotifier 要求状态不可变。

isLoading 控制加载指示器,error 显示错误信息,hasReachedMax 用于判断是否已加载全部数据(支持分页加载)。

3. Notifier 实现

dart 复制代码
class UsersNotifier extends StateNotifier<UsersState> {
  final UserRepository _userRepository;
  int _page = 1;
  bool _isLoading = false;

  UsersNotifier(this._userRepository) : super(const UsersState());

  Future<void> fetchUsers({bool refresh = false}) async {
    if (_isLoading) return;

    if (refresh) {
      _page = 1;
      state = const UsersState(isLoading: true);
    } else {
      state = state.copyWith(isLoading: true);
    }

    try {
      _isLoading = true;
      
      final response = await _userRepository.getUsers(page: _page);
      
      if (response.success && response.hasData) {
        final users = response.data!;
        
        state = state.copyWith(
          users: refresh ? users : [...state.users, ...users],
          isLoading: false,
          error: null,
          hasReachedMax: users.length < 20, // 假设每页20条
        );
        
        _page++;
      } else {
        state = state.copyWith(
          isLoading: false,
          error: response.message ?? '加载失败',
        );
      }
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: '网络请求失败: $e',
      );
    } finally {
      _isLoading = false;
    }
  }

  Future<void> refresh() => fetchUsers(refresh: true);
}

详细说明:状态驱动 UI 的完整流程

UsersNotifier 是业务逻辑的核心,负责协调 Repository 并更新状态。

  • fetchUsers 方法根据 refresh 参数决定是刷新还是加载更多。
  • 请求前设置 isLoading: true,触发 UI 显示加载动画。
  • 成功后合并新旧数据(分页加载),失败则更新 error 字段。
  • 使用 _isLoading 标志防止重复请求,提升稳定性。
  • refresh() 方法简化下拉刷新调用。

该模式实现了"请求 → 状态更新 → UI 重绘"的闭环,是现代 Flutter 应用的标准范式。

五、数据模型与序列化

1. 自动生成序列化代码

dart 复制代码
// user.dart
import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final int id;
  final String name;
  final String email;
  final String? avatar;
  final DateTime createdAt;
  final DateTime updatedAt;

  User({
    required this.id,
    required this.name,
    required this.email,
    this.avatar,
    required this.createdAt,
    required this.updatedAt,
  });

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);

  User copyWith({
    int? id,
    String? name,
    String? email,
    String? avatar,
    DateTime? createdAt,
    DateTime? updatedAt,
  }) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      email: email ?? this.email,
      avatar: avatar ?? this.avatar,
      createdAt: createdAt ?? this.createdAt,
      updatedAt: updatedAt ?? this.updatedAt,
    );
  }
}

详细说明:json_serializable 的高效与安全

手动编写 fromJsontoJson 容易出错且繁琐。json_annotation + build_runner 可以自动生成这些方法。

  • @JsonSerializable() 注解标记类需要序列化。
  • part 'user.g.dart' 指定生成文件。
  • 运行 dart run build_runner build 自动生成 fromJsontoJson
  • copyWith 支持局部更新,常用于状态管理。

这种方式既保证了性能,又减少了人为错误。

2. 通用响应模型

dart 复制代码
// api_response_model.dart
@JsonSerializable(genericArgumentFactories: true)
class ApiResponseModel<T> {
  final int code;
  final String message;
  final T? data;

  ApiResponseModel({
    required this.code,
    required this.message,
    this.data,
  });

  factory ApiResponseModel.fromJson(
    Map<String, dynamic> json,
    T Function(Object? json) fromJsonT,
  ) =>
      _$ApiResponseModelFromJson(json, fromJsonT);

  Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
      _$ApiResponseModelToJson(this, toJsonT);

  bool get isSuccess => code == 200;
}

详细说明:泛型响应模型的通用性

许多后端 API 返回统一结构,如 { code: 200, message: "ok", data: {} }

ApiResponseModel<T> 封装了这种结构:

  • 支持泛型 T,可嵌套任意数据模型。
  • genericArgumentFactories: true 允许传递 fromJsonT 函数,实现嵌套对象解析。
  • isSuccess 快速判断请求是否成功。

该模型可直接用于 dio 的响应解析,减少重复代码。

六、Repository 模式实现

dart 复制代码
// user_repository.dart
class UserRepository {
  final ApiService _apiService;

  UserRepository(this._apiService);

  Future<ApiResponse<List<User>>> getUsers({int page = 1, int limit = 20}) async {
    return _apiService.get(
      '/users',
      queryParameters: {
        'page': page,
        'limit': limit,
      },
      fromJson: (data) {
        if (data is List) {
          return data.map((e) => User.fromJson(e)).toList();
        }
        return [];
      },
    );
  }

  Future<ApiResponse<User>> getUserById(int id) async {
    return _apiService.get(
      '/users/$id',
      fromJson: (data) => User.fromJson(data),
    );
  }

  Future<ApiResponse<User>> updateUser(User user) async {
    return _apiService.post(
      '/users/${user.id}',
      data: user.toJson(),
      fromJson: (data) => User.fromJson(data),
    );
  }

  Future<ApiResponse<String>> uploadAvatar(String filePath) async {
    return _apiService.upload<String>(
      '/users/avatar',
      filePath,
      fromJson: (data) => data['url'] as String,
    );
  }
}

详细说明:Repository 模式的分层价值

Repository 是业务逻辑与数据源之间的抽象层,具有以下作用:

  • 隔离变化:如果未来更换网络库(如从 dio 改为 http),只需修改 Repository,不影响上层。
  • 统一入口 :所有用户相关请求集中在 UserRepository,便于维护。
  • 数据转换:将原始 API 响应转换为 App 内部模型。
  • 支持多数据源:未来可轻松扩展本地数据库、缓存等。

它是实现 Clean Architecture 的关键一环。

七、UI 集成与使用

1. 网络请求状态 Widget

dart 复制代码
// network_state_widget.dart
class NetworkStateWidget<T> extends StatelessWidget {
  final AsyncValue<T> asyncValue;
  final Widget Function(T data) successBuilder;
  final Widget Function()? loadingBuilder;
  final Widget Function(Object error, StackTrace stackTrace)? errorBuilder;
  final VoidCallback? onRetry;

  const NetworkStateWidget({
    super.key,
    required this.asyncValue,
    required this.successBuilder,
    this.loadingBuilder,
    this.errorBuilder,
    this.onRetry,
  });

  @override
  Widget build(BuildContext context) {
    return asyncValue.when(
      data: successBuilder,
      loading: loadingBuilder ?? () => const _LoadingWidget(),
      error: errorBuilder ?? (error, stack) => _ErrorWidget(
        error: error,
        onRetry: onRetry,
      ),
    );
  }
}

class _LoadingWidget extends StatelessWidget {
  const _LoadingWidget();

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16),
            Text('加载中...'),
          ],
        ),
      ),
    );
  }
}

class _ErrorWidget extends StatelessWidget {
  final Object error;
  final VoidCallback? onRetry;

  const _ErrorWidget({
    required this.error,
    this.onRetry,
  });

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              Icons.error_outline,
              size: 64,
              color: Theme.of(context).colorScheme.error,
            ),
            const SizedBox(height: 16),
            Text(
              error.toString(),
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    color: Theme.of(context).colorScheme.error,
                  ),
            ),
            const SizedBox(height: 16),
            if (onRetry != null)
              FilledButton(
                onPressed: onRetry,
                child: const Text('重试'),
              ),
          ],
        ),
      ),
    );
  }
}

详细说明:状态感知型 UI 组件的设计理念

NetworkStateWidget 是一个通用的状态渲染组件,接受 AsyncValue(Riverpod 提供)并根据其状态展示不同 UI。

  • when 方法分别处理 dataloadingerror 三种状态。
  • 支持自定义加载和错误界面。
  • 提供 onRetry 回调,点击"重试"按钮可重新发起请求。

这种组件可复用于任何异步操作(如网络请求、数据库查询),极大提升 UI 开发效率。

2. 页面使用示例

dart 复制代码
// users_page.dart
class UsersPage extends ConsumerStatefulWidget {
  const UsersPage({super.key});

  @override
  ConsumerState<UsersPage> createState() => _UsersPageState();
}

class _UsersPageState extends ConsumerState<UsersPage> {
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
    // 初始加载数据
    WidgetsBinding.instance.addPostFrameCallback((_) {
      ref.read(usersProvider.notifier).fetchUsers();
    });
  }

  void _onScroll() {
    if (_scrollController.position.pixels ==
        _scrollController.position.maxScrollExtent) {
      // 加载更多
      ref.read(usersProvider.notifier).fetchUsers();
    }
  }

  @override
  Widget build(BuildContext context) {
    final usersState = ref.watch(usersProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('用户列表'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => ref.read(usersProvider.notifier).refresh(),
          ),
        ],
      ),
      body: NetworkStateWidget(
        asyncValue: AsyncValue.data(usersState),
        onRetry: () => ref.read(usersProvider.notifier).refresh(),
        successBuilder: (state) {
          return RefreshIndicator(
            onRefresh: () => ref.read(usersProvider.notifier).refresh(),
            child: ListView.builder(
              controller: _scrollController,
              itemCount: state.users.length + (state.hasReachedMax ? 0 : 1),
              itemBuilder: (context, index) {
                if (index >= state.users.length) {
                  return const Padding(
                    padding: EdgeInsets.all(16.0),
                    child: Center(child: CircularProgressIndicator()),
                  );
                }
                
                final user = state.users[index];
                return UserListItem(user: user);
              },
            ),
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
}

详细说明:完整页面的构建逻辑

UsersPage 展示了如何将网络、状态、UI 三层无缝集成:

  • 使用 ConsumerStatefulWidget 访问 Riverpod 状态。
  • initState 中监听滚动事件,实现上拉加载更多。
  • ref.watch(usersProvider) 监听状态变化,自动刷新 UI。
  • NetworkStateWidget 统一处理加载、成功、错误状态。
  • RefreshIndicator 支持下拉刷新。
  • 列表末尾显示加载更多指示器。

整个页面逻辑清晰,用户体验流畅,是典型的生产级实现。

八、性能优化与最佳实践

1. 请求取消与防抖

dart 复制代码
class Debouncer {
  final Duration delay;
  Timer? _timer;

  Debouncer({required this.delay});

  void call(void Function() action) {
    _timer?.cancel();
    _timer = Timer(delay, action);
  }

  void dispose() {
    _timer?.cancel();
  }
}

class SearchService {
  final ApiService _apiService;
  CancelToken? _cancelToken;

  SearchService(this._apiService);

  Future<ApiResponse<List<User>>> searchUsers(String query) async {
    // 取消之前的请求
    _cancelToken?.cancel('New search request');
    _cancelToken = CancelToken();

    return _apiService.get(
      '/users/search',
      queryParameters: {'q': query},
      cancelToken: _cancelToken,
      fromJson: (data) {
        if (data is List) {
          return data.map((e) => User.fromJson(e)).toList();
        }
        return [];
      },
    );
  }
}

详细说明:防抖与请求取消的必要性

  • 防抖(Debouncer):在搜索框等高频输入场景中,避免每次输入都发起请求,仅在用户停止输入后延迟执行,减少服务器压力。
  • 取消请求(CancelToken):当新请求发起时,取消旧请求,防止旧响应覆盖新数据(即"竞态条件")。

两者结合可显著提升性能和用户体验。

2. 缓存策略

dart 复制代码
class CacheManager {
  static final CacheManager _instance = CacheManager._internal();
  factory CacheManager() => _instance;
  CacheManager._internal();

  final Map<String, dynamic> _memoryCache = {};
  final Duration _defaultDuration = const Duration(minutes: 5);

  Future<T?> get<T>(String key) async {
    final cached = _memoryCache[key];
    if (cached != null && cached is _CacheItem) {
      if (cached.expiry.isAfter(DateTime.now())) {
        return cached.data as T;
      } else {
        _memoryCache.remove(key);
      }
    }
    return null;
  }

  Future<void> set<T>(String key, T data, {Duration? duration}) async {
    _memoryCache[key] = _CacheItem(
      data: data,
      expiry: DateTime.now().add(duration ?? _defaultDuration),
    );
  }

  Future<void> remove(String key) async {
    _memoryCache.remove(key);
  }

  Future<void> clear() async {
    _memoryCache.clear();
  }
}

class _CacheItem {
  final dynamic data;
  final DateTime expiry;

  _CacheItem({required this.data, required this.expiry});
}

详细说明:内存缓存的实现与应用场景

CacheManager 提供了一个简单的内存缓存系统,支持:

  • 设置过期时间,避免数据陈旧。
  • 自动清理过期项。
  • 支持任意类型数据存储。

可用于缓存用户信息、配置项、搜索结果等不常变动的数据,减少网络请求,加快响应速度。

3. 网络状态监控

dart 复制代码
class NetworkConnectivity {
  final Connectivity _connectivity = Connectivity();
  final StreamController<bool> _connectionStatusController = 
      StreamController<bool>.broadcast();

  Stream<bool> get connectionStatus => _connectionStatusController.stream;

  Future<void> initialize() async {
    _connectivity.onConnectivityChanged.listen((result) {
      _updateConnectionStatus(result);
    });
    
    // 初始检查
    final initialResult = await _connectivity.checkConnectivity();
    _updateConnectionStatus(initialResult);
  }

  void _updateConnectionStatus(ConnectivityResult result) {
    final hasConnection = result != ConnectivityResult.none;
    _connectionStatusController.add(hasConnection);
  }

  Future<bool> get isConnected async {
    final result = await _connectivity.checkConnectivity();
    return result != ConnectivityResult.none;
  }

  void dispose() {
    _connectionStatusController.close();
  }
}

详细说明:实时网络状态监控的重要性

通过 connectivity_plus 插件,可以监听设备网络状态变化。

  • 应用启动时检查网络是否可用。
  • 在无网络时禁用请求或提示用户。
  • 网络恢复后自动重试失败的请求。
  • 提升离线体验和应用健壮性。

是构建高质量 App 的必备功能。

九、配置与依赖

1. pubspec.yaml 配置

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter

  # 网络请求
  dio: ^5.0.0
  retrofit: ^4.0.1
  logger: ^1.1.0
  
  # 状态管理
  flutter_riverpod: ^2.3.0
  
  # 序列化
  json_annotation: ^4.8.1
  
  # 网络状态监控
  connectivity_plus: ^5.0.0
  
  # 本地存储
  shared_preferences: ^2.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  
  # 序列化代码生成
  build_runner: ^2.4.0
  retrofit_generator: ^4.0.1
  json_serializable: ^6.7.1

详细说明:核心依赖的选择依据

  • dio:核心网络库,功能全面。
  • flutter_riverpod:现代状态管理方案,支持 Provider 注入。
  • json_serializable:自动化 JSON 序列化,减少样板代码。
  • connectivity_plus:跨平台网络状态检测。
  • shared_preferences:轻量级本地存储,用于保存 token 等。
  • build_runner:代码生成工具,配合 json_serializable 使用。

这些库共同构成了一个现代化 Flutter 应用的技术底座。

十、总结

通过这种深度集成的网络请求架构,你可以构建出:

  • 高性能:支持缓存、防抖、并发控制
  • 可维护:清晰的分层结构(API → Repository → StateNotifier → UI)
  • 易测试:依赖注入 + 抽象接口,便于单元测试和 Mock
  • 健壮性高:统一错误处理、自动重试、Token 刷新机制

关键建议

  • 根据项目规模选择合适的抽象层级(小项目可简化 Repository 层)
  • 合理使用拦截器进行日志、鉴权、错误处理
  • 结合 Riverpod 实现状态驱动 UI 更新
  • 善用 json_serializable 减少模板代码
  • 始终考虑用户体验:加载、错误、重试、离线支持

构建一个现代化的 Flutter 应用,从设计好网络层开始!

相关推荐
消失的旧时光-19432 小时前
Flutter Scaffold 全面解析:打造页面骨架的最佳实践(附场景示例 + 踩坑分享)
前端·flutter
Q688238864 小时前
三菱Q系列PLC大型自动化生产线程序案例分享
flutter
消失的旧时光-19434 小时前
Flutter 布局入门
flutter
天天开发1 天前
Flutter每日库: image_picker选取相册图片视频
flutter
消失的旧时光-19431 天前
Flutter 组件:StatelessWidget vs StatefulWidget
flutter
天意__1 天前
Flutter 聊天界面使用ListView的reverse:true,导致条目太少的时候会从下往上显示,导致顶部大片空白
flutter
汤面不加鱼丸1 天前
flutter实践:混合app在部分android旧机型上显示异常
android·flutter
火柴就是我1 天前
flutter 为什么大家说不能在initState 方法中调用dependOnInheritedWidgetOfExactType
flutter