Dio Query - 现代网络请求封装库

基于 Dio 的现代化网络请求封装库,提供统一异常处理、智能缓存、Token 管理和重试机制等企业级功能。

核心特性

  • 统一异常处理 - 规范化异常类型,提供统一错误格式
  • 智能缓存系统 - 支持内存/持久化缓存、过期管理、优先级策略
  • Token 自动管理 - 自动注入 Token,支持自定义格式
  • 灵活重试机制 - 指数/线性/固定延迟,智能抖动避免重试风暴
  • 数据解析器 - 内置多种解析器,支持自定义解析和分页
  • Mock 支持 - 动态 Mock、场景管理、模板生成
  • 请求队列 - 并发控制、优先级管理、批量取消
  • 完整拦截器 - 日志、Token、连通性、响应、错误处理

快速开始

1. 初始化配置

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await NetworkManager.initialize(
    NetworkConfig(
      baseUrl: 'https://api.example.com',
      connectTimeout: const Duration(seconds: 30),
      receiveTimeout: const Duration(seconds: 30),
      // 全局错误处理
      onError: (exception) {
        debugPrint('错误: ${exception.message}');
      },
      // 401 未授权处理
      onUnauthorized: (exception) async {
        TokenManager.instance.clearToken();
        // 导航到登录页
      },
      // Token 格式(可选)
      tokenFormatConfig: TokenFormatConfig.bearer(),
    ),
    cacheConfig: CacheInitConfig(
      enableAutoClean: true,
      autoCleanInterval: const Duration(hours: 1),
      maxCacheSizeMB: 100,
    ),
  );

  runApp(MyApp());
}

2. 基础请求

final client = NetworkClient.instance;

// GET 请求
final user = await client.get<User>('/api/user');

// POST 请求
final result = await client.post<ApiResponse>(
  '/api/login',
  data: {'username': 'user', 'password': 'pass'},
);

// PUT/PATCH/DELETE
await client.put('/api/user/1', data: {...});
await client.patch('/api/user/1', data: {...});
await client.delete('/api/user/1');

3. 高级功能

// 缓存 + 重试
final data = await client.get<User>(
  '/api/user',
  cacheConfig: CacheConfig.withExpiration(Duration(hours: 1)),
  retryConfig: RetryConfig(
    strategy: RetryStrategy.exponential,
    maxAttempts: 3,
  ),
);

// 不使用 Token(公开接口)
await client.post(
  '/auth/login',
  data: {'username': 'user'},
  tokenConfig: TokenConfig.noToken,
);

// 自定义 BaseUrl
await client.get('/api/data', baseUrl: 'https://cdn.example.com');

// 进度监控
await client.post(
  '/api/upload',
  data: formData,
  onSendProgress: (count, total) {
    print('上传: ${(count / total * 100).toStringAsFixed(0)}%');
  },
);

// 请求取消
final cancelToken = CancelToken();
final request = client.get('/api/user', cancelToken: cancelToken);
cancelToken.cancel('用户取消');

核心功能详解

Token 管理

// 设置 Token
TokenManager.instance.setToken('your-token');

// 动态获取 Token
TokenManager.instance.setTokenGetter(() {
  return prefs.getString('auth_token');
});

// 清除 Token
TokenManager.instance.clearToken();

// Token 格式配置
TokenFormatConfig.bearer()  // Authorization: Bearer {token}
TokenFormatConfig.custom('X-Auth-Token')  // X-Auth-Token: {token}

缓存策略

// 预设配置
CacheConfig.noCache                              // 不使用缓存
CacheConfig.withExpiration(Duration(hours: 1))   // 1小时过期
CacheConfig.highPriority(Duration(days: 1))      // 高优先级,最后清理
CacheConfig.lowPriority(Duration(minutes: 30))   // 低优先级,优先清理

// 自定义配置
CacheConfig(
  expirationTime: Duration(hours: 2),
  forceRefresh: false,
  useCacheWhenOffline: true,
  priority: 7,  // 0-10,数值越大优先级越高
)

// 缓存管理
await CacheManager.cleanExpired();  // 清理过期
await CacheManager.clear();         // 清空所有
final stats = await CacheManager.getStats();  // 统计信息

重试策略

// 预设配置
RetryConfig.aggressive()    // 激进重试,快速重试
RetryConfig.conservative()  // 保守重试,较长延迟
RetryConfig.networkOnly()   // 仅网络错误重试

// 自定义配置
RetryConfig(
  strategy: RetryStrategy.exponential,  // 指数退避:1s → 2s → 4s
  maxAttempts: 3,
  enableJitter: true,  // 启用抖动,避免重试风暴
  onRetry: (attempt, error, delay) {
    print('第 $attempt 次重试,延迟 ${delay.inSeconds}秒');
    showSnackBar('网络不稳定,正在重试...');
  },
  retryableStatusCodes: [408, 429, 500, 502, 503, 504],
)

// 重试策略说明
RetryStrategy.exponential  // 指数退避:1s → 2s → 4s → 8s
RetryStrategy.linear       // 线性退避:2s → 4s → 6s → 8s
RetryStrategy.fixed        // 固定间隔:2s → 2s → 2s
RetryStrategy.immediate    // 立即重试:0s → 0s → 0s

数据解析器

// 内置解析器
DirectParser<T>()           // 直接返回原始数据
DataFieldParser<T>()        // 从 data 字段提取
ResultFieldParser<T>()      // 从 result 字段提取
ListDataParser<T>(...)      // 列表解析
PaginatedListParser<T>(...) // 分页列表解析

// 使用示例
final user = await client.get<User>(
  '/api/user',
  parser: DataFieldParser<User>(),
);

// 列表解析
final newsList = await client.get<List<News>>(
  '/api/news',
  parser: ListDataParser<News>(
    itemParser: (item) => News.fromJson(item),
  ),
);

// 分页解析
final result = await client.get<PaginatedResult<Product>>(
  '/api/products',
  parser: PaginatedListParser<Product>(
    itemParser: (item) => Product.fromJson(item),
    paginationFields: {
      'currentPage': 'page',
      'total': 'total',
      'pageSize': 'size',
    },
  ),
);
print('第 ${result.meta?.currentPage} 页,共 ${result.meta?.total} 条');

// 自定义解析器
final data = await client.get<CustomData>(
  '/api/custom',
  parser: CustomParser<CustomData>((rawData) {
    return CustomData.fromJson(rawData['custom_field']);
  }),
);

异常处理

// 异常类型
enum NetworkExceptionType {
  networkError,   // 网络错误
  timeout,        // 请求超时
  unauthorized,   // 未授权(401)
  forbidden,      // 禁止访问(403)
  notFound,       // 资源不存在(404)
  serverError,    // 服务器错误(500+)
  parseError,     // 数据解析错误
  unknown,        // 未知错误
}

// 全局处理
  NetworkConfig(
    onError: (exception) {
      switch (exception.type) {
        case NetworkExceptionType.networkError:
          showError('网络连接失败');
          break;
        case NetworkExceptionType.timeout:
          showError('请求超时');
          break;
        case NetworkExceptionType.unauthorized:
        navigateToLogin();
          break;
      // ... 其他类型
      }
    },
)

// 局部处理
try {
  final user = await client.get<User>('/api/user');
} on NetworkException catch (e) {
  if (e.type == NetworkExceptionType.networkError) {
      // 处理网络错误
  }
}

Mock 功能

// 启用 Mock
MockManager().enable();

// 静态 Mock
MockManager().register(MockData.static(
  path: '/api/user',
  method: 'GET',
  data: MockTemplates.success(data: {'id': 1, 'name': '张三'}),
  delay: Duration(seconds: 1),
));

// 动态 Mock(根据参数生成)
MockManager().register(MockData.dynamic(
  path: '/api/user',
  method: 'GET',
  generator: (method, path, queryParams, data) {
    final userId = queryParams?['id'] ?? '1';
    return MockTemplates.success(
      data: {
        'id': userId,
        'name': '用户$userId',
        'email': 'user$userId@example.com',
      },
    );
  },
));

// 场景管理
MockManager().registerAll([
  // 成功场景
  MockData.static(
    path: '/api/login',
    method: 'POST',
    data: MockTemplates.success(data: {'token': 'success-token'}),
    scenario: 'success',
  ),
  // 失败场景
  MockData.static(
    path: '/api/login',
    method: 'POST',
    data: MockTemplates.error(message: '密码错误', code: 401),
    statusCode: 401,
    scenario: 'error',
  ),
]);

// 切换场景
MockManager().setScenario('success');  // 使用成功场景
MockManager().setScenario('error');    // 使用失败场景

// Mock 模板
MockTemplates.success(data: {...})                    // 成功响应
MockTemplates.error(message: '错误', code: 500)       // 错误响应
MockTemplates.list([...])                             // 列表响应
MockTemplates.paginatedList(                          // 分页响应
  list: [...],
  page: 1,
  pageSize: 10,
  total: 100,
)

// 禁用 Mock
MockManager().disable();

请求队列

// 启用队列(默认最大并发 6)
RequestQueue.instance.enable(maxConcurrent: 6);

// 优先级控制
await client.get(
  '/api/important',
  priority: RequestPriority.critical,  // critical > high > normal > low
);

// 标签和批量取消
Future<void> loadPageData() async {
  final tag = 'page-1';
  
  await Future.wait([
    client.get('/api/user', queueTag: tag),
    client.get('/api/posts', queueTag: tag),
    client.get('/api/comments', queueTag: tag),
  ]);
}

// 页面切换时取消所有相关请求
@override
void dispose() {
  RequestQueue.instance.cancelByTag('page-1');
  super.dispose();
}

// 队列控制
RequestQueue.instance.pause();              // 暂停队列
RequestQueue.instance.resume();             // 恢复队列
RequestQueue.instance.clear();              // 清空队列
RequestQueue.instance.setMaxConcurrent(10); // 设置最大并发

// 队列监控
final stats = RequestQueue.instance.getStats();
print('等待: ${stats.pending}, 运行: ${stats.running}');

// 监听队列变化
RequestQueue.instance.queueStream.listen((stats) {
  print('队列更新: $stats');
});

// 禁用队列(请求直接执行)
RequestQueue.instance.disable();

配置参数

NetworkConfig 参数

参数 类型 默认值 说明
baseUrl String 必填 API 基础地址
connectTimeout Duration 30s 连接超时
receiveTimeout Duration 30s 接收超时
onError Function null 全局错误处理器
onUnauthorized Function null 401 处理器
tokenFormatConfig TokenFormatConfig null Token 格式配置
parser DataParser DataFieldParser 默认解析器
cacheStrategyType CacheStrategyType memory 缓存策略
enableTokenInterceptor bool true 启用 Token 拦截器
enableLoggerInterceptor bool? null 启用日志(null=自动)
enableConnectivityInterceptor bool true 启用连通性检查
enableResponseInterceptor bool true 启用响应拦截器
enableErrorInterceptor bool true 启用错误拦截器

拦截器执行顺序

拦截器按以下顺序执行(从外到内):

请求流程: Logger → Token → Connectivity → Response → Error → 网络请求
响应流程: 网络响应 → Error → Response → Connectivity → Token → Logger

使用场景

场景 1:标准 REST API

// 用户登录
final response = await client.post<Map<String, dynamic>>(
  '/auth/login',
  data: {'username': username, 'password': password},
  tokenConfig: TokenConfig.noToken,
);
TokenManager.instance.setToken(response['token']);

// 获取用户信息(自动带 Token)
final user = await client.get<User>(
  '/api/user/profile',
  cacheConfig: CacheConfig.withExpiration(Duration(hours: 1)),
);

// 更新用户信息
await client.put<void>(
  '/api/user/profile',
  data: user.toJson(),
);

场景 2:列表页批量加载

// 控制并发数,避免同时加载太多图片
RequestQueue.instance.enable(maxConcurrent: 3);

for (final item in items) {
  client.get(
    item.imageUrl,
    priority: RequestPriority.low,
    queueTag: 'list-images',
  );
}

场景 3:文件上传

final formData = FormData.fromMap({
  'file': await MultipartFile.fromFile(filePath),
  'name': 'avatar.jpg',
});

await client.post(
  '/api/upload',
  data: formData,
  priority: RequestPriority.high,
  onSendProgress: (count, total) {
    final progress = (count / total * 100).toStringAsFixed(0);
    print('上传进度: $progress%');
  },
);

场景 4:Riverpod 集成

@riverpod
Future<User> userProfile(UserProfileRef ref) async {
  final client = NetworkClient.instance;
  return await client.get<User>(
    '/api/user/profile',
    cacheConfig: CacheConfig.withExpiration(Duration(hours: 1)),
    retryConfig: RetryConfig.aggressive(),
  );
}

@riverpod
Future<PaginatedResult<Post>> posts(
  PostsRef ref, {
  required int page,
}) async {
  final client = NetworkClient.instance;
  return await client.get<PaginatedResult<Post>>(
    '/api/posts',
    queryParameters: {'page': page, 'size': 20},
    parser: PaginatedListParser<Post>(
      itemParser: (item) => Post.fromJson(item),
    ),
  );
}

性能优化建议

  1. 合理使用缓存

    • 静态数据:高优先级长期缓存
    • 动态数据:低优先级短期缓存
    • 及时清理过期缓存
  2. 选择合适的重试策略

    • 网络不稳定:指数退避 + 抖动
    • 服务器过载:保守重试
    • 快速失败:不重试或仅 1 次
  3. 使用请求队列

    • 控制并发数,避免资源浪费
    • 设置请求优先级,优先加载关键数据
    • 页面切换时批量取消请求
  4. 及时取消请求

    • 使用 CancelToken 取消不需要的请求
    • 页面销毁时取消所有相关请求
  5. 使用多 BaseUrl

    • 静态资源走 CDN,提高加载速度

注意事项

  1. 缓存策略

    • 默认内存缓存,应用重启后丢失
    • 需要持久化时使用 CacheStrategyType.drift
  2. 重试机制

    • 默认只在网络错误、超时时重试
    • HTTP 4xx/5xx 错误需配置 retryableStatusCodes
  3. Token 管理

    • 登录等公开接口使用 TokenConfig.noToken
    • 推荐使用 Token Getter 动态获取
  4. 日志拦截器

    • Debug 模式自动启用,Release 模式自动禁用
    • 可通过 enableLoggerInterceptor 手动控制
  5. 请求队列

    • 默认禁用,需手动启用
    • 队列暂停后,新请求会进入等待状态

常见问题

Q: 请求取消后会抛出异常吗?
A: 会抛出 DioException,类型为 DioExceptionType.cancel。建议捕获该异常。

Q: 重试时会触发全局错误处理器吗?
A: 每次重试失败都会触发,最后一次失败后不再重试。

Q: 如何自定义响应数据格式?
A: 使用 CustomParser 或继承 DataParser 实现自定义解析逻辑。

Q: Mock 数据会影响生产环境吗?
A: 不会。Mock 默认禁用,只在手动调用 MockManager().enable() 后生效。

Q: 队列模式和普通模式有什么区别?
A: 队列模式可以控制并发数和优先级,普通模式直接发起请求。

更新日志

v1.3.0 (最新)

新增功能:

  • ✨ 请求队列管理系统(并发控制、优先级、标签管理)
  • ✨ 队列统计和监控
  • ✨ Stream 流式监听队列变化

v1.2.0

新增功能:

  • ✨ 数据解析器优化(移除冗余参数、新增 PaginatedListParser)
  • ✨ Mock 功能增强(动态 Mock、场景管理、模板生成)
  • ✨ 自定义匹配器

破坏性变更:

  • ⚠️ DataParser.parse() 方法签名变更(移除 path 参数)

v1.1.0

新增功能:

  • ✨ CancelToken 支持
  • ✨ 多 BaseUrl 支持
  • ✨ 上传下载进度回调
  • ✨ 重试机制增强(抖动、更多状态码、回调)
  • ✨ 缓存键生成器优化

v1.0.0

  • 🎉 初始版本发布

许可证

MIT License