Skip to content

Flutter Dio 网络请求封装设计文档

导航目录

本文讲解一个基于 Dio 的 Flutter 网络层封装方案,目标是提供统一请求入口可控的 UI 交互(Loading/Toast)一致的错误处理,并支持 Token 自动附加与过期刷新(含并发排队)。

整体架构

shell
lib/
├── app/
   ├── data/
   └── providers/ # API 提供者
       ├── api_constants.dart  # 统一管理 API 相关常量
       ├── api_exceptions.dart # 定义网络请求异常类型
       ├── api_interceptors.dart # 实现请求拦截器核心逻辑
       └── api_provider.dart   # 提供统一请求接口

核心组件

ApiConstants:常量与约定

建议把“协议约定 + UI 控制开关 + 错误文案”集中管理,避免散落在业务代码里。

dart
class ApiConstants {
  // 基础配置
  static String get baseUrl => env('API_BASE_URL'); // 环境变量配置
  static const Duration defaultTimeout = Duration(milliseconds: 5000);

  // 请求头配置
  static const String headerTokenKey = 'X-Token';
  static const String contentType = 'application/json;charset=UTF-8';

  // 请求控制参数
  static const String extraIsLoading = 'isLoading'; // 显示加载框
  static const String extraIsToast = 'isToast';     // 显示错误提示
  static const String extraIsCancelReq = 'isCancelReq'; // 可取消请求
  static const String extraIsDeduplication = 'isDeduplication'; // 请求去重

  // 错误信息
  static const String tokenExpiredCode = '0200002';
  static const String tokenRefreshFailed = 'Token 刷新失败,请重新登录';
  static const String networkTimeoutMessage = '网络连接超时,请检查网络后重试';
  static const String defaultErrorMessage = '请求失败,请稍后重试';
  static const String networkErrorMessage = '网络异常,请检查网络后重试';
  static const String certificateErrorMessage = '证书校验失败';
  static const String unknownErrorMessage = '未知错误';
}

ApiExceptions:异常模型

  • HttpException:对外统一抛出的异常类型(UI 层/业务层只需要处理这一种即可)。
  • ReturnWithResponse:用于“在拦截器内部提前返回一个响应”的场景(例如 Token 刷新后直接把重试结果作为当前请求的结果返回)。
dart
/// HTTP 请求异常基类
class HttpException implements Exception {
  final String message;
  final DioException? dioError;
  final dynamic responseData;

  HttpException(this.message, {this.dioError, this.responseData});
}

/// 携带响应数据的特殊异常(用于 Token 刷新后重试)
class ReturnWithResponse implements Exception {
  final Response response;
  ReturnWithResponse(this.response);
}

ApiInterceptors:拦截器核心

拦截器负责把“跨业务的通用能力”集中在一起:Loading 控制Token 附加请求去重可取消请求统一错误转换Token 过期刷新

请求前:统一预处理

dart
Future<void> onRequest(RequestOptions options) async {
  _handleLoading(options); // 处理加载状态
  await _attachToken(options); // 附加 Token
  _checkDuplicateRequest(options); // 请求去重
  _setupCancelToken(options); // 设置取消Token
}

Token 附加

dart
Future<void> _attachToken(RequestOptions options) async {
  final AuthModel authModel = await AuthServices.get();
  if (authModel.token.isNotEmpty) {
    options.headers[ApiConstants.headerTokenKey] = authModel.token;
  }
}

Token 过期刷新(并发排队)

关键点:当 Token 过期时,可能有多个请求同时失败。此时应当 只发起一次刷新,其它请求进入队列等待刷新完成,然后自动重试

dart
Future<Response> _handleTokenExpired(RequestOptions options) async {
  if (isRefreshing) {
    // 已有刷新请求时加入等待队列
    final completer = Completer<Response>();
    waitingCompleters.add(completer);
    await completer.future;
    return await dio.fetch(options..cancelToken = null);
  }

  isRefreshing = true;
  try {
    // 执行Token刷新
    AuthModel authModel = await AuthServices.get();
    AuthModel newAuthModel = await AuthRepository.refresh(authModel.refreshToken);

    // 保存新Token
    await AuthServices.set(newAuthModel);
    options.headers[ApiConstants.headerTokenKey] = newAuthModel.token;

    // 重试原始请求
    final newResponse = await dio.fetch(options..cancelToken = null);

    // 完成所有等待中的请求
    for (final c in waitingCompleters) {
      if (!c.isCompleted) c.complete(newResponse);
    }

    return newResponse;
  } finally {
    isRefreshing = false;
    waitingCompleters.clear();
  }
}

ApiProvider:统一请求入口

初始化配置(BaseOptions + 拦截器)

dart
void _initInstance() {
  _dio.options = BaseOptions(
    baseUrl: ApiConstants.baseUrl,
    connectTimeout: ApiConstants.defaultTimeout,
    headers: {'Content-Type': ApiConstants.contentType},
  );

  // 调试模式添加日志
  if (kDebugMode) {
    _dio.interceptors.add(PrettyDioLogger(/* 配置 */));
  }

  // 添加拦截器
  _dio.interceptors.add(InterceptorsWrapper(
    onRequest: _interceptors.onRequest,
    onResponse: _interceptors.onResponse,
    onError: _interceptors.onError,
  ));
}

统一请求方法(推荐:所有请求都走这一条)

约定:通过 Options.extra 把 UI 行为与控制参数传入拦截器,让“是否显示 Loading/Toast、是否可取消、是否去重”变成每个请求的可配置项,而不是散落在业务层。

dart
static Future<T> request<T>(
  String path, {
  String method = 'POST',
  dynamic data,
  bool isLoading = true,
  bool isToast = true,
  bool isCancelReq = true,
  bool isDeduplication = true,
  T Function(dynamic)? fromJson,
}) async {
  try {
    final isGet = method.toUpperCase() == 'GET';
    final response = await _instance._dio.request(
      path,
      data: isGet ? null : data,
      queryParameters: isGet ? data : null,
      options: Options(
        method: method,
        extra: {
          ApiConstants.extraIsLoading: isLoading,
          ApiConstants.extraIsToast: isToast,
          ApiConstants.extraIsCancelReq: isCancelReq,
          ApiConstants.extraIsDeduplication: isDeduplication,
        },
      ),
    );
    return _instance.parseResponse<T>(response.data['data'], fromJson);
  } on DioException catch (e) {
    // 统一错误处理
    final message = e.message ?? ApiConstants.defaultErrorMessage;
    if (isToast) ToastUtil.show(message);
    throw HttpException(message, dioError: e);
  }
}

关键特性

Token 自动管理

  • 请求前自动添加 Token 到请求头
  • Token 过期时自动刷新
  • 刷新期间并发请求排队处理
  • 刷新成功后自动更新请求头并重试

请求控制

  • 去重机制:防止重复提交
  • 取消功能:全局请求取消支持
  • 加载状态:自动显示/隐藏加载框
  • 错误提示:统一错误消息处理

错误处理体系

建议把 Dio 的错误类型转换成“用户能理解的文案”,并把服务端错误(badResponse)进一步解析为业务错误信息。

dart
String _getErrorMessage(DioException error) {
  switch (error.type) {
    case DioExceptionType.connectionTimeout:
      return ApiConstants.networkTimeoutMessage;
    case DioExceptionType.badResponse:
      return _parseServerError(error.response!);
    case DioExceptionType.connectionError:
      return ApiConstants.networkErrorMessage;
    case DioExceptionType.badCertificate:
      return ApiConstants.certificateErrorMessage;
    default:
      return '${ApiConstants.unknownErrorMessage}: ${error.error}';
  }
}

响应解析(类型安全)

重点:网络层只负责“把 data 解析成目标类型”,不直接依赖业务模型。通过 fromJson 把解析策略交给调用方。

dart
T parseResponse<T>(dynamic data, T Function(dynamic)? fromJson) {
  if (fromJson != null) return fromJson(data);
  if (data is T) return data;
  throw HttpException('Response type mismatch');
}

使用示例

初始化

dart
void main() {
  ApiProvider.init(); // 初始化网络请求
  runApp(MyApp());
}

发起请求

dart
// GET 请求:把 data 当成 queryParameters
final products = await ApiProvider.request<List<Product>>(
  '/products',
  method: 'GET',
  data: {'page': 1, 'size': 20},
  fromJson: (json) =>
      (json as List).map((e) => Product.fromJson(e)).toList(),
  isLoading: true,
);

// POST 请求:把 data 当成 body
final result = await ApiProvider.request<String>(
  '/orders',
  method: 'POST',
  data: order.toJson(),
  isToast: true,
);

错误处理

dart
try {
  await ApiProvider.request('/submit', method: 'POST', data: formData);
} on HttpException catch (e) {
  print('请求失败: ${e.message}');
  // 自定义错误处理
}

最佳实践

  1. 环境配置

    • 使用 .env 文件管理不同环境 API 地址
    • 通过 env('API_BASE_URL') 动态获取配置
  2. 请求参数

    • 重要操作启用去重 (isDeduplication: true)
    • 关键表单提交谨慎启用“可取消”(需要强一致的提交可考虑 isCancelReq: false
    • 后台请求关闭加载框 (isLoading: false)
  3. Token 管理

    • 使用 AuthServices 统一管理认证状态
    • 确保 AuthRepository.refresh() 正确实现
  4. 错误处理

    • 用户操作使用 isToast: true 自动提示
    • 关键错误使用自定义处理逻辑