Flutter网络请求实战:Dio拦截器、重试与缓存机制深度解析
引言:为什么我们选择Dio?
在Flutter开发中,处理网络请求几乎是每个应用都绕不开的环节。虽然官方提供了基础的 http 包,但在实际项目中,尤其是业务复杂的企业级应用里,我们总会遇到更多的需求:比如统一的请求和响应处理、自动重试失败请求、灵活管理缓存,以及一套清晰的错误处理机制。这个时候,Dio 凭借其强大的拦截器系统和高度可扩展的设计,就成了很多开发者的首选。
今天,我们就来深入聊聊 Dio 的几个核心高级功能:拦截器机制 、请求重试策略 以及数据缓存实现。我会结合原理分析、可运行的代码示例,并分享一些性能优化上的建议,希望能帮你搭建一个更稳健、易维护的网络层。
一、理解Dio的核心:拦截器是如何工作的
1.1 设计精髓:拦截器链
Dio 的强大,很大程度上源于它采用的 拦截器链(Interceptor Chain) 模式。你可以把它想象成一条流水线:每个拦截器都是一个独立的工作站,负责完成某个特定任务(比如加签、日志、鉴权),而请求和响应则会依次经过这些站点。
整个流程大致如下:
[ 发起请求 ]
↓
[ 请求拦截器A ] → 添加日志、统一修改请求头
↓
[ 请求拦截器B ] → Token 管理、参数签名
↓
...
↓
[ Dio 执行引擎 ] → 真正发出网络请求
↓
[ 响应拦截器N ] → 统一格式化数据、处理特定错误
↓
...
↓
[ 响应拦截器A ] → 解析数据、更新本地 Token、缓存结果
↓
[ 结果返回给调用者 ]
这种设计的好处很明显:它把像日志、认证这样的"横切关注点"从核心业务逻辑里剥离出来,每个拦截器只做一件事,代码更干净,也更容易维护和测试。
1.2 拦截器的类型与执行顺序
Dio 的拦截器主要分两种:
- 全局拦截器 :通过
dio.interceptors.add()添加,对这个 Dio 实例发出的所有请求都生效。 - 单次请求拦截器:可以在发起某个具体请求时临时添加,只针对这一次请求。它的优先级比全局拦截器更高。
关于执行顺序,这里有个关键点需要记住:
- 请求阶段 :拦截器按照添加的先后顺序依次执行。
- 响应阶段 :顺序刚好反过来,按照添加的倒序执行。
- 错误处理:如果链条中任何一个环节抛出异常,后续的拦截器会被跳过,直接进入错误处理流程。
二、动手搭建一个企业级的拦截器系统
理论说完了,我们来点实际的。下面我会用一个完整的示例,带你实现三个最常用、也最实用的拦截器。
2.1 项目初始化与基础配置
首先,在 pubspec.yaml 里把需要的依赖添加上:
yaml
dependencies:
flutter:
sdk: flutter
dio: ^5.0.0
shared_preferences: ^2.2.0 # 用来做持久化缓存(示例)
logger: ^2.0.0 # 输出美观的日志
crypto: ^3.0.0 # 生成缓存Key(示例)
接着,我们创建一个网络管理的单例类 lib/service/network_manager.dart:
dart
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// 自定义的网络异常类,用来统一应用内的错误格式
class NetworkException implements Exception {
final String message;
final int? statusCode;
final String? errorCode;
NetworkException(this.message, {this.statusCode, this.errorCode});
@override
String toString() => 'NetworkException: $message (status: $statusCode, code: $errorCode)';
}
/// 核心网络管理类(单例)
class NetworkManager {
static final NetworkManager _instance = NetworkManager._internal();
late final Dio _dio;
final Logger _logger = Logger();
late final SharedPreferences _prefs;
factory NetworkManager() => _instance;
NetworkManager._internal() {
_initDio();
}
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
void _initDio() {
_dio = Dio(
BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com', // 示例API地址
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
sendTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
},
),
);
// 按照顺序添加全局拦截器
_dio.interceptors.add(LoggingInterceptor(_logger)); // 1. 日志记录
_dio.interceptors.add(AuthInterceptor(_prefs)); // 2. 认证处理
_dio.interceptors.add(ErrorInterceptor()); // 3. 统一错误处理
// 注意响应返回时的顺序是 3 -> 2 -> 1
}
Dio get dio => _dio;
}
2.2 拦截器一:智能日志拦截器
创建 lib/service/interceptors/logging_interceptor.dart。这个拦截器会在开发阶段输出详细日志,方便调试;在生产环境则只记录错误,避免信息泄露和性能损耗。
dart
import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
/// 智能日志拦截器
class LoggingInterceptor extends Interceptor {
final Logger logger;
LoggingInterceptor(this.logger);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 只在非发布模式打印详细请求日志
if (!kReleaseMode) {
logger.i('🌍 [DIO] Request => ${options.method.toUpperCase()} ${options.uri}');
if (options.queryParameters.isNotEmpty) {
logger.v('Query Parameters: ${options.queryParameters}');
}
if (options.data != null) {
logger.v('Request Body: ${options.data}');
}
if (options.headers.isNotEmpty) {
logger.v('Headers: ${options.headers}');
}
}
super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (!kReleaseMode) {
logger.i('✅ [DIO] Response <= ${response.statusCode} ${response.requestOptions.uri}');
logger.v('Response Body: ${response.data}');
}
super.onResponse(response, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
// 错误日志在任何环境下都建议记录
logger.e(
'❌ [DIO] Error <= ${err.type} | ${err.response?.statusCode} ${err.requestOptions.uri}',
error: err.error,
stackTrace: err.stackTrace,
);
if (err.response != null) {
logger.e('Error Response Data: ${err.response?.data}');
}
super.onError(err, handler);
}
}
2.3 拦截器二:认证与Token管理拦截器
这是处理用户认证的核心。它自动为请求添加Token,并在遇到401/403错误时尝试刷新Token,然后重试之前失败的请求。
创建 lib/service/interceptors/auth_interceptor.dart:
dart
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// 认证拦截器:负责Token的添加与刷新
class AuthInterceptor extends Interceptor {
static const String _tokenKey = 'auth_token';
static const String _refreshTokenKey = 'refresh_token';
final SharedPreferences _prefs;
bool _isRefreshing = false;
final List<({RequestOptions options, ErrorInterceptorHandler handler})> _failedRequestsQueue = [];
AuthInterceptor(this._prefs);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 从本地读取Token并添加到请求头
final token = _prefs.getString(_tokenKey);
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
// 如果需要,可以在这里设置URL白名单,跳过某些不需要认证的请求
super.onRequest(options, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 只处理认证相关的错误
if (_shouldRefreshToken(err)) {
final requestOptions = err.requestOptions;
// 将失败的请求暂存到队列中
_failedRequestsQueue.add((options: requestOptions, handler: handler));
if (!_isRefreshing) {
_isRefreshing = true;
try {
// 尝试刷新Token
final newToken = await _refreshToken();
if (newToken != null) {
// 刷新成功,保存新Token
_prefs.setString(_tokenKey, newToken);
// 重试队列里所有失败的请求
_retryFailedRequests();
} else {
// 刷新失败,可能是Refresh Token也失效了,清空队列并通知用户重新登录
_clearQueueWithError(handler, '登录已过期,请重新登录。');
}
} catch (e) {
_clearQueueWithError(handler, 'Token刷新失败: $e');
} finally {
_isRefreshing = false;
}
}
// 注意:这里先不调用 handler.next/reject,等待Token刷新后重试
} else {
// 如果不是认证错误,直接传递给下一个拦截器
super.onError(err, handler);
}
}
bool _shouldRefreshToken(DioException err) {
return err.response?.statusCode == 401 || err.response?.statusCode == 403;
}
Future<String?> _refreshToken() async {
// 这里模拟刷新Token的请求,实际项目中需要对接你的后端API
final refreshToken = _prefs.getString(_refreshTokenKey);
if (refreshToken == null) return null;
// 特别注意:刷新Token时要用一个全新的、不带认证拦截器的Dio实例,避免循环拦截
final refreshDio = Dio();
try {
final response = await refreshDio.post<Map<String, dynamic>>(
'/auth/refresh', // 你的刷新Token接口地址
data: {'refresh_token': refreshToken},
);
return response.data?['access_token'] as String?;
} catch (e) {
return null;
}
}
void _retryFailedRequests() {
final queue = List.from(_failedRequestsQueue);
_failedRequestsQueue.clear();
for (final item in queue) {
_retryRequest(item.options, item.handler);
}
}
void _retryRequest(RequestOptions options, ErrorInterceptorHandler handler) {
// 使用我们主NetworkManager的Dio实例(此时已经携带了新Token)重新发送请求
NetworkManager().dio.request(
options.path,
data: options.data,
queryParameters: options.queryParameters,
options: Options(
method: options.method,
headers: options.headers,
),
).then(
(response) => handler.resolve(response),
onError: (error) => handler.reject(error as DioException),
);
}
void _clearQueueWithError(ErrorInterceptorHandler handler, String message) {
final queue = List.from(_failedRequestsQueue);
_failedRequestsQueue.clear();
for (final item in queue) {
item.handler.reject(
DioException(
requestOptions: item.options,
error: NetworkException(message),
),
);
}
// 最后也拒绝当前这个错误
handler.reject(
DioException(
requestOptions: handler.requestOptions,
error: NetworkException(message),
),
);
}
}
2.4 拦截器三:统一错误处理拦截器
最后一个拦截器负责"兜底",把Dio的各种原生异常转换为我们应用内部统一的格式,这样上层业务调用起来会非常方便。
创建 lib/service/interceptors/error_interceptor.dart:
dart
import 'package:dio/dio.dart';
/// 统一错误处理拦截器
class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final NetworkException networkException;
switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
networkException = NetworkException(
'网络请求超时,请检查网络连接。',
statusCode: err.response?.statusCode,
errorCode: 'TIMEOUT',
);
break;
case DioExceptionType.badResponse:
final statusCode = err.response?.statusCode;
final data = err.response?.data;
String message = '服务器错误 ($statusCode)';
String? errorCode;
// 尝试从响应体中解析出更具体的错误信息
if (data is Map<String, dynamic>) {
message = data['message']?.toString() ?? message;
errorCode = data['code']?.toString();
} else if (data is String) {
message = data;
}
networkException = NetworkException(
message,
statusCode: statusCode,
errorCode: errorCode,
);
break;
case DioExceptionType.cancel:
networkException = NetworkException('请求已被取消。', errorCode: 'CANCELLED');
break;
case DioExceptionType.unknown:
if (err.error?.toString().contains('SocketException') == true) {
networkException = NetworkException('网络连接不可用。', errorCode: 'NO_CONNECTION');
} else {
networkException = NetworkException('未知错误: ${err.error}', errorCode: 'UNKNOWN');
}
break;
case DioExceptionType.badCertificate:
networkException = NetworkException('SSL证书验证失败。', errorCode: 'BAD_CERTIFICATE');
break;
case DioExceptionType.connectionError:
networkException = NetworkException('无法连接到服务器。', errorCode: 'CONNECTION_ERROR');
break;
}
// 用我们自定义的异常替换掉原始异常
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: networkException,
response: err.response,
type: err.type,
),
);
}
}
三、进阶功能:让网络层更健壮
有了稳固的拦截器基础,我们可以再往上添砖加瓦,实现请求重试和智能缓存,让你的应用在网络不稳定的环境下也能有更好的表现。
3.1 实现指数退避重试策略
在网络请求中,偶尔的失败是难免的。对于因网络抖动导致的短暂失败,自动重试是个很好的补救措施。下面这个拦截器实现了经典的指数退避算法,并加入了随机抖动,避免多个请求同时重试造成"惊群效应"。
创建 lib/service/retry_handler.dart:
dart
import 'dart:async';
import 'dart:math';
import 'package:dio/dio.dart';
/// 自定义重试拦截器
class RetryInterceptor extends Interceptor {
final int maxRetries;
final Duration baseDelay;
final Random _random = Random();
RetryInterceptor({this.maxRetries = 3, this.baseDelay = const Duration(seconds: 1)});
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
final requestOptions = err.requestOptions;
final extra = requestOptions.extra;
// 检查这个请求是否开启了重试机制
final shouldRetry = extra['retryEnabled'] ?? true;
if (!shouldRetry) {
return super.onError(err, handler);
}
// 检查已经重试了几次
final retryCount = (extra['retryCount'] as int?) ?? 0;
if (retryCount >= maxRetries) {
return super.onError(err, handler);
}
// 只对特定的错误类型进行重试(比如超时、连接错误)
if (!_shouldRetryForErrorType(err.type)) {
return super.onError(err, handler);
}
// 计算等待时间:指数退避 + 随机抖动
final delay = _calculateDelay(retryCount);
await Future.delayed(delay);
// 更新重试计数
requestOptions.extra['retryCount'] = retryCount + 1;
try {
// 重新发起请求
final response = await NetworkManager().dio.request(
requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: Options(
method: requestOptions.method,
headers: requestOptions.headers,
extra: requestOptions.extra,
contentType: requestOptions.contentType,
responseType: requestOptions.responseType,
),
);
handler.resolve(response);
} catch (e) {
// 如果重试再次失败,继续传递错误(会再次进入这个拦截器)
super.onError(
e is DioException ? e : DioException(requestOptions: requestOptions, error: e),
handler,
);
}
}
bool _shouldRetryForErrorType(DioExceptionType type) {
// 主要针对网络层面的可恢复错误进行重试
return {
DioExceptionType.connectionTimeout,
DioExceptionType.receiveTimeout,
DioExceptionType.sendTimeout,
DioExceptionType.connectionError,
DioExceptionType.unknown, // 对unknown类型要谨慎,最好结合具体错误信息判断
}.contains(type);
}
Duration _calculateDelay(int retryCount) {
// 指数退避公式:delay = baseDelay * 2^retryCount
final exponentialDelay = baseDelay * pow(2, retryCount).toInt();
// 添加±20%的随机抖动
final jitter = exponentialDelay.inMilliseconds * 0.2 * (_random.nextDouble() * 2 - 1);
final finalDelay = exponentialDelay.inMilliseconds + jitter;
return Duration(milliseconds: finalDelay.toInt());
}
}
// 使用方法:在NetworkManager的_initDio方法里添加
// _dio.interceptors.add(RetryInterceptor(maxRetries: 3));
3.2 实现智能多层缓存策略
对于一些不常变化、但又需要频繁读取的数据(比如用户头像、配置信息、文章列表),合理的缓存能极大提升用户体验并减轻服务器压力。下面的缓存管理器支持内存和持久化两级缓存,并提供了灵活的缓存策略。
创建 lib/service/cache_manager.dart:
dart
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// 缓存项的数据结构
class CacheItem {
final String key;
final String data;
final DateTime createdAt;
final DateTime expiresAt;
final String? etag;
final Map<String, dynamic>? headers;
CacheItem({
required this.key,
required this.data,
required this.createdAt,
required this.expiresAt,
this.etag,
this.headers,
});
bool get isExpired => DateTime.now().isAfter(expiresAt);
// 序列化与反序列化方法
factory CacheItem.fromJson(Map<String, dynamic> json) {
return CacheItem(
key: json['key'],
data: json['data'],
createdAt: DateTime.parse(json['createdAt']),
expiresAt: DateTime.parse(json['expiresAt']),
etag: json['etag'],
headers: json['headers'] != null ? Map<String, dynamic>.from(json['headers']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'key': key,
'data': data,
'createdAt': createdAt.toIso8601String(),
'expiresAt': expiresAt.toIso8601String(),
'etag': etag,
'headers': headers,
};
}
}
/// 智能缓存管理器
class CacheManager {
static const String _cachePrefix = 'dio_cache_';
final SharedPreferences _prefs;
final Map<String, CacheItem> _memoryCache = {};
final Duration _defaultCacheDuration;
CacheManager(this._prefs, {Duration? defaultCacheDuration})
: _defaultCacheDuration = defaultCacheDuration ?? const Duration(minutes: 5);
/// 根据请求信息生成唯一的缓存Key
String _generateCacheKey(RequestOptions options) {
final keyStr = '${options.method}:${options.uri}';
if (options.data != null) {
keyStr += ':${jsonEncode(options.data)}';
}
final bytes = utf8.encode(keyStr);
return _cachePrefix + sha256.convert(bytes).toString();
}
/// 获取缓存
Future<CacheItem?> getCache(RequestOptions options) async {
final cacheKey = _generateCacheKey(options);
final extra = options.extra;
// 1. 检查这个请求是否允许缓存
final cacheEnabled = extra['cacheEnabled'] ?? true;
if (!cacheEnabled) return null;
// 2. 先查内存缓存(最快)
final memoryItem = _memoryCache[cacheKey];
if (memoryItem != null && !memoryItem.isExpired) {
return memoryItem;
}
// 3. 查持久化存储(如SharedPreferences)
final cachedJson = _prefs.getString(cacheKey);
if (cachedJson != null) {
try {
final cacheItem = CacheItem.fromJson(jsonDecode(cachedJson));
if (!cacheItem.isExpired) {
// 还没过期,把它加载到内存中,下次就快了
_memoryCache[cacheKey] = cacheItem;
return cacheItem;
} else {
// 过期了,清理掉
await _prefs.remove(cacheKey);
}
} catch (e) {
// 数据格式不对,也清理掉
await _prefs.remove(cacheKey);
}
}
return null;
}
/// 保存响应到缓存
Future<void> saveCache(RequestOptions options, Response response) async {
final cacheKey = _generateCacheKey(options);
final extra = options.extra;
// 读取缓存策略和持续时间
final cachePolicy = extra['cachePolicy'] ?? CachePolicy.normal;
final cacheDuration = extra['cacheDuration'] as Duration? ?? _defaultCacheDuration;
if (cachePolicy == CachePolicy.noCache) return;
final now = DateTime.now();
final expiresAt = now.add(cacheDuration);
// 如果服务器支持,可以保存ETag用于后续的缓存校验
final etag = response.headers.value('etag');
final cacheItem = CacheItem(
key: cacheKey,
data: jsonEncode(response.data), // 假设响应数据是可JSON序列化的
createdAt: now,
expiresAt: expiresAt,
etag: etag,
headers: response.headers.map,
);
// 存到内存
_memoryCache[cacheKey] = cacheItem;
// 如果需要持久化,异步保存到SharedPreferences(避免阻塞主线程)
if (cachePolicy == CachePolicy.persistent) {
final jsonStr = jsonEncode(cacheItem.toJson());
unawaited(_prefs.setString(cacheKey, jsonStr));
}
}
/// 清理所有过期的缓存
Future<void> cleanExpiredCache() async {
final allKeys = _prefs.getKeys().where((key) => key.startsWith(_cachePrefix));
for (final key in allKeys) {
final cachedJson = _prefs.getString(key);
if (cachedJson != null) {
try {
final cacheItem = CacheItem.fromJson(jsonDecode(cachedJson));
if (cacheItem.isExpired) {
await _prefs.remove(key);
_memoryCache.remove(key);
}
} catch (e) {
await _prefs.remove(key);
}
}
}
}
/// 根据Key删除特定缓存
Future<void> removeCache(String key) async {
final fullKey = key.startsWith(_cachePrefix) ? key : _cachePrefix + key;
await _prefs.remove(fullKey);
_memoryCache.remove(fullKey);
}
/// 清空所有缓存
Future<void> clearAllCache() async {
final allKeys = _prefs.getKeys().where((key) => key.startsWith(_cachePrefix));
for (final key in allKeys) {
await _prefs.remove(key);
}
_memoryCache.clear();
}
}
/// 缓存策略
enum CachePolicy {
noCache, // 不缓存
normal, // 只存在内存里,应用重启就消失
persistent, // 持久化到本地存储
}
(文章后续还可以继续添加"缓存拦截器"的实现部分,以及总结