这篇文章适合谁
- 想在 Flutter 端实现“请求治理”(并发限流、优先级、重试、取消)的同学
- 正在被“重复点击”“批量上传”“弱网重试”“页面切换取消请求”等问题困扰的团队
- 需要一套统一 API(收敛错误、统一取消、给出可观测状态)的工程实践
一、核心能力一图读懂(Why + What)
- 请求任务化管理:每个请求是一个“任务”,有生命周期、可取消、可重试。
- 并发控制 + 优先级队列:保障吞吐且让“重要请求先走”。
- 智能重试:失败自动重试、指数退避、防雪崩、自定义可重试判定。
- 取消体系:按任务、按标签、全部取消;页面切换超好用。
- 统一返回:全部返回
ApiResult<T>,成功/失败/取消均可感知。 - 轻量异步锁:用 Future 链式锁顺序化关键修改,避免竞态。
关键实现参考:
class RequestStrategyManager {
static RequestStrategyManager? _instance;
static RequestStrategyManager get instance =>
_instance ??= RequestStrategyManager._internal();
final Dio _dio;
int maxConcurrency;
final Map<String, RequestTask> _runningTasks = {};
final List<RequestTask> _pendingTasks = [];
final Map<String, RequestTask> _allTasks = {};
Completer<void> _lock = Completer<void>()..complete();
二、关键概念与原理(知其然,知其所以然)
- 请求任务 RequestTask:承载请求执行体、取消令牌、重试次数、开始/结束时间、完成器。
class RequestTask<T> {
final String id;
final Future<ApiResult<T>> Function() requestFunction;
final RequestConfig? config;
final CancelToken? cancelToken;
final Completer<ApiResult<T>> completer;
int retryCount = 0;
- 策略参数 RequestConfig:重试次数、重试延迟、指数退避、优先级、标签、可重试判定。
class RequestConfig {
final int maxRetries;
final int retryDelay;
final bool useExponentialBackoff;
final int priority;
final String? tag;
final bool Function(ApiException)? shouldRetry;
int calculateRetryDelay(int attempt) {
if (useExponentialBackoff) {
return retryDelay * (1 << attempt);
}
return retryDelay;
}
}
- 优先级队列(插入排序):规模小、实现简单、足够好用;必要时可替换二叉堆。
void _insertIntoPendingQueue(RequestTask task) {
final priority = task.config?.priority ?? 0;
int insertIndex = 0;
for (int i = 0; i < _pendingTasks.length; i++) {
final taskPriority = _pendingTasks[i].config?.priority ?? 0;
if (taskPriority < priority) {
insertIndex = i;
break;
} else {
insertIndex = i + 1;
}
}
_pendingTasks.insert(insertIndex, task);
}
- 指数退避重试:保护后端与网络环境,避免“立刻重试”造成洪峰。
if (!result.success && _shouldRetry(task, result)) {
task.retryCount++;
final config = task.config ?? const RequestConfig();
if (task.retryCount <= config.maxRetries) {
final delay = config.calculateRetryDelay(task.retryCount - 1);
await Future.delayed(Duration(milliseconds: delay));
if (task.isCancelled) { ... }
_processTask(task);
return;
}
}
- 轻量异步锁:用 Future 串行关键区,避免并发修改共享状态。
Future<T> _withLock<T>(Future<T> Function() action) async {
final previousLock = _lock;
final newLock = Completer<void>();
_lock = newLock;
await previousLock.future;
try {
return await action();
} finally {
newLock.complete();
}
}
三、如何使用(从 0 到 1)
下面的示例代码都是“可直接粘贴改造”的模板。统一入口是 RequestStrategyManager.instance。
1) 发起一个 GET 请求(含 JSON 解析)
class Car {
final String id;
final String name;
Car({required this.id, required this.name});
factory Car.fromJson(Map<String, dynamic> json) =>
Car(id: json['id'].toString(), name: json['name'] as String);
}
Future<void> loadCarDetail(String carId) async {
final result = await RequestStrategyManager.instance.get<Car>(
'/api/cars/$carId',
queryParameters: {'lang': 'zh-CN'},
fromJson: (data) => Car.fromJson(data as Map<String, dynamic>),
config: const RequestConfig(
maxRetries: 2,
retryDelay: 800, // ms
useExponentialBackoff: true,
priority: 5, // 高优先级
tag: 'car_detail',
),
);
if (result.success) {
final car = result.data!;
// 使用 car
} else {
// 统一错误提示:result.message / result.code 取决于 ApiResult 定义
}
}
要点:
-
fromJson用来把response.data转换为你的模型。 -
RequestConfig即插即用,不填则用默认值(最多重试 3 次,1s 起步、指数退避)。
2) POST/PUT/DELETE 一样用
final result = await RequestStrategyManager.instance.post<bool>(
'/api/order/submit',
data: {'itemId': '123', 'count': 2},
fromJson: (data) => true, // 服务端只需校验成功则返回 true
config: const RequestConfig(priority: 10, tag: 'order'),
);
3) 文件上传(支持 onSendProgress)
final upload = await RequestStrategyManager.instance.uploadFile<String>(
'/api/upload',
'/path/to/image.jpg',
fieldName: 'file',
data: {'bizType': 'avatar'},
onSendProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(1);
// 更新 UI 进度
},
fromJson: (data) => (data as Map<String, dynamic>)['url'] as String,
config: const RequestConfig(tag: 'avatar_upload', priority: 8),
);
4) 批量请求(并发收敛 + 统一结果)
final reqs = <Future<ApiResult<dynamic>>>[
RequestStrategyManager.instance.get('/api/a'),
RequestStrategyManager.instance.get('/api/b'),
RequestStrategyManager.instance.get('/api/c'),
];
final results = await RequestStrategyManager.instance.batchRequest(reqs);
for (final r in results) {
if (r.success) {
// 成功处理
} else {
// 个别失败照常可见
}
}
四、取消策略(页面级极其好用)
- 单个任务取消:获取任务 id 后取消(通常内部托管,外部很少直接拿 id)。
- 按标签取消:适合“页面发出的所有请求用同一 tag”,离开页面时一键取消。
- 全部取消:例如需要紧急终止所有网络活动。
接口参考:
Future<bool> cancelTask(String taskId, [String? reason]) async { ... }
591:624:/Users/mac/flutter_projects/rental/ddcar_rental/lib/core/services/request_strategy_manager.dart
Future<int> cancelTag(String tag, [String? reason]) async { ... }
627:652:/Users/mac/flutter_projects/rental/ddcar_rental/lib/core/services/request_strategy_manager.dart
Future<void> cancelAll([String? reason]) async { ... }
页面集成建议(以 Riverpod/BLoC/普通 Stateful 为例):
// 页面进入时:发起请求并统一打上 tag
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
await RequestStrategyManager.instance.get(
'/api/page/data',
config: const RequestConfig(tag: 'page_home', priority: 3),
);
}
// 页面退出时:一键取消该页面发起的所有请求
void dispose() {
RequestStrategyManager.instance.cancelTag('page_home', '页面关闭');
super.dispose();
}
五、重试策略的业务化落地
默认逻辑:网络错误/超时/服务器错误可重试,不超过 maxRetries。
你可以结合业务语义做“可重试判定”:
final cfg = RequestConfig(
maxRetries: 3,
retryDelay: 1000,
useExponentialBackoff: true,
shouldRetry: (ApiException e) {
// 例:401 不重试,引导重新登录;429/5xx 重试;业务 4xx 不重试
if (e.statusCode == 401) return false;
if (e.statusCode == 429) return true;
if (e.statusCode != null && e.statusCode! >= 500) return true;
return e.isNetworkError || e.isTimeout;
},
);
为什么:
- 不是所有失败都应重试。对“不可恢复”的业务错误(如参数错、权限错),应立即反馈而非放大流量。
- 指数退避能避免“重试风暴”,让系统有自愈窗口。
六、并发与优先级调优
- 查看状态
final status = await RequestStrategyManager.instance.getStatus();
// { runningCount, pendingCount, totalCount, maxConcurrency, runningTasks, pendingTasks }
- 动态调整最大并发(比如进入大图列表页)
await RequestStrategyManager.instance.updateMaxConcurrency(6);
经验值(端上 IO 密集,网络请求为主):
- maxConcurrency:4–8 区间通常稳健
- priority:数字越大优先级越高;关键链路(登录/下单)给高优先级
- retry:2–3 次足矣;delay 起步 500–1000ms,指数退避开启
七、常见坑与规避
- 无限重试陷阱:必须设置
maxRetries并正确识别“不可重试错误”。 - 假并发卡死:不要在
requestFunction中做同步重计算,保持请求异步。 - 取消未清理:避免绕开管理器直接用 Dio 发请求;统一从管理器入口发起,保证清理一致性。
- 优先级误解:当前实现是“数字越大越优先”。
- 大量 Pending:若大量任务排队,考虑提升并发、或分批调度(分段发起)。
八、模板化配置建议
- 列表页弱网加载
const RequestConfig listConfig = RequestConfig(
maxRetries: 2,
retryDelay: 700,
useExponentialBackoff: true,
priority: 2,
tag: 'page_list',
);
- 关键下单链路
const RequestConfig orderConfig = RequestConfig(
maxRetries: 3,
retryDelay: 800,
useExponentialBackoff: true,
priority: 10,
tag: 'order',
);
- 批量上传(页面离开可一键取消)
const RequestConfig uploadConfig = RequestConfig(
maxRetries: 2,
retryDelay: 1000,
useExponentialBackoff: true,
priority: 6,
tag: 'upload_batch',
);
九、与“裸 Dio + 业务重试”的对比
- 统一治理:把“并发、重试、取消、优先级、状态观测”统一收口,避免每处重复轮子。
- 上层更纯净:页面/业务只表述“要干啥”,复杂度在网络层内聚。
- 可观测性:随时拿到
runningCount/pendingCount等指标,便于诊断性能问题。
十、快速检查清单(落地时照这个对齐)
- 给每类页面设置独立
tag,在dispose里cancelTag(tag)。 - 给关键链路设置更高
priority。 - 默认开启指数退避;
retryDelay ≥ 500ms,maxRetries ≤ 3。 - 对 401/403/明确业务 4xx 设为“不重试”。
- 有批量任务时用
batchRequest或按批次发起。 - 监控
getStatus(),发现 pending 激增则调大并发或分段发。
十一、FAQ
-
问:
ApiResult<T>有哪些字段?如何判断成功?- 答:本工程中统一通过
result.success判断;成功时result.data可用;失败时result.message/result.error/result.code可读。保持上层只面向ApiResult即可。
- 答:本工程中统一通过
-
问:如何拿到任务 id 做单个取消?
- 答:管理器内部会托管任务 id 并在完成时返回结果。实际工程中更推荐使用
tag做页面/批量级别取消,更贴合使用场景。
- 答:管理器内部会托管任务 id 并在完成时返回结果。实际工程中更推荐使用
-
问:优先级会“饿死”低优先级吗?
- 答:在极端高并发且持续有高优先级涌入时可能出现。可用“配额/分批”策略或限制高优先级占比来平衡。
十二、结语
- 通过任务化抽象、优先级队列、并发控制、指数退避、自定义判定和统一取消,
RequestStrategyManager把“不可控的网络”收束成“可治理的系统”。 - 你可以从“为页面统一打 tag + 配置合理重试 + 设置并发上限”三件事开始,立刻提升稳定性与可维护性。