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 次
-
使用请求队列
- 控制并发数,避免资源浪费
- 设置请求优先级,优先加载关键数据
- 页面切换时批量取消请求
-
及时取消请求
- 使用
CancelToken取消不需要的请求 - 页面销毁时取消所有相关请求
- 使用
-
使用多 BaseUrl
- 静态资源走 CDN,提高加载速度
注意事项
-
缓存策略
- 默认内存缓存,应用重启后丢失
- 需要持久化时使用
CacheStrategyType.drift
-
重试机制
- 默认只在网络错误、超时时重试
- HTTP 4xx/5xx 错误需配置
retryableStatusCodes
-
Token 管理
- 登录等公开接口使用
TokenConfig.noToken - 推荐使用 Token Getter 动态获取
- 登录等公开接口使用
-
日志拦截器
- Debug 模式自动启用,Release 模式自动禁用
- 可通过
enableLoggerInterceptor手动控制
-
请求队列
- 默认禁用,需手动启用
- 队列暂停后,新请求会进入等待状态
常见问题
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
Libraries
- core/client/network_client
- core/config/network_config
- core/constants/network_constants
- core/exceptions/network_exception
- dio_query
- features/cache/cache_config
- features/cache/cache_key_generator
- features/cache/cache_manager
- features/cache/cache_strategies
- features/cache/cache_strategy
- features/cache/database
- features/mock/mock_config
- features/parser/data_parser
- features/queue/request_queue
- features/retry/retry_config
- features/token/token_config
- features/token/token_manager
- interceptors/connectivity_interceptor
- interceptors/error_interceptor
- interceptors/logger_interceptor
- interceptors/mock_interceptor
- interceptors/response_interceptor
- interceptors/token_interceptor