Appearance
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}');
// 自定义错误处理
}最佳实践
环境配置:
- 使用
.env文件管理不同环境 API 地址 - 通过
env('API_BASE_URL')动态获取配置
- 使用
请求参数:
- 重要操作启用去重 (
isDeduplication: true) - 关键表单提交谨慎启用“可取消”(需要强一致的提交可考虑
isCancelReq: false) - 后台请求关闭加载框 (
isLoading: false)
- 重要操作启用去重 (
Token 管理:
- 使用
AuthServices统一管理认证状态 - 确保
AuthRepository.refresh()正确实现
- 使用
错误处理:
- 用户操作使用
isToast: true自动提示 - 关键错误使用自定义处理逻辑
- 用户操作使用