Flutter艺术探索-Dio网络请求库:拦截器、重试与缓存

Flutter网络请求实战:Dio拦截器、重试与缓存机制深度解析

引言:为什么我们选择Dio?

在Flutter开发中,处理网络请求几乎是每个应用都绕不开的环节。虽然官方提供了基础的 http 包,但在实际项目中,尤其是业务复杂的企业级应用里,我们总会遇到更多的需求:比如统一的请求和响应处理、自动重试失败请求、灵活管理缓存,以及一套清晰的错误处理机制。这个时候,Dio 凭借其强大的拦截器系统和高度可扩展的设计,就成了很多开发者的首选。

今天,我们就来深入聊聊 Dio 的几个核心高级功能:拦截器机制请求重试策略 以及数据缓存实现。我会结合原理分析、可运行的代码示例,并分享一些性能优化上的建议,希望能帮你搭建一个更稳健、易维护的网络层。

一、理解Dio的核心:拦截器是如何工作的

1.1 设计精髓:拦截器链

Dio 的强大,很大程度上源于它采用的 拦截器链(Interceptor Chain) 模式。你可以把它想象成一条流水线:每个拦截器都是一个独立的工作站,负责完成某个特定任务(比如加签、日志、鉴权),而请求和响应则会依次经过这些站点。

整个流程大致如下:

复制代码
[ 发起请求 ]
     ↓
[ 请求拦截器A ] → 添加日志、统一修改请求头
     ↓
[ 请求拦截器B ] → Token 管理、参数签名
     ↓
      ...
     ↓
[ Dio 执行引擎 ] → 真正发出网络请求
     ↓
[ 响应拦截器N ] → 统一格式化数据、处理特定错误
     ↓
      ...
     ↓
[ 响应拦截器A ] → 解析数据、更新本地 Token、缓存结果
     ↓
[ 结果返回给调用者 ]

这种设计的好处很明显:它把像日志、认证这样的"横切关注点"从核心业务逻辑里剥离出来,每个拦截器只做一件事,代码更干净,也更容易维护和测试。

1.2 拦截器的类型与执行顺序

Dio 的拦截器主要分两种:

  1. 全局拦截器 :通过 dio.interceptors.add() 添加,对这个 Dio 实例发出的所有请求都生效。
  2. 单次请求拦截器:可以在发起某个具体请求时临时添加,只针对这一次请求。它的优先级比全局拦截器更高。

关于执行顺序,这里有个关键点需要记住:

  • 请求阶段 :拦截器按照添加的先后顺序依次执行。
  • 响应阶段 :顺序刚好反过来,按照添加的倒序执行。
  • 错误处理:如果链条中任何一个环节抛出异常,后续的拦截器会被跳过,直接进入错误处理流程。

二、动手搭建一个企业级的拦截器系统

理论说完了,我们来点实际的。下面我会用一个完整的示例,带你实现三个最常用、也最实用的拦截器。

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,   // 持久化到本地存储
}

(文章后续还可以继续添加"缓存拦截器"的实现部分,以及总结

相关推荐
Miguo94well6 小时前
Flutter框架跨平台鸿蒙开发——每日早报APP开发流程
flutter·华为·harmonyos·鸿蒙
小白阿龙7 小时前
鸿蒙+flutter 跨平台开发——回看历史APP的开发流程
flutter·华为·harmonyos
Miguo94well7 小时前
Flutter框架跨平台鸿蒙开发——每日饮水APP的开发流程
flutter·华为·harmonyos
鸣弦artha8 小时前
Flutter框架跨平台鸿蒙开发——Image Widget加载状态管理
android·flutter
新镜8 小时前
【Flutter】Slider 自定义trackShape时最大最小值无法填满进度条问题
flutter
2501_944526428 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 主题切换实现
android·开发语言·javascript·python·flutter·游戏·django
kirk_wang8 小时前
Flutter艺术探索-RESTful API集成:Flutter后端对接实战
flutter·移动开发·flutter教程·移动开发教程
某zhuan8 小时前
Flutter环境搭建(VS Code和Android Studio)
android·flutter·android studio
小雨下雨的雨9 小时前
触手可及的微观世界:基于 Flutter 的 3D 血细胞交互教学应用开发
flutter·3d·华为·矩阵·交互·harmonyos·鸿蒙系统