Flutter构建可控、可重试、可取消的网络层:全面掌握 RequestStrategyManager

这篇文章适合谁

  • 想在 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,在 disposecancelTag(tag)
  • 给关键链路设置更高 priority
  • 默认开启指数退避;retryDelay ≥ 500msmaxRetries ≤ 3
  • 对 401/403/明确业务 4xx 设为“不重试”。
  • 有批量任务时用 batchRequest 或按批次发起。
  • 监控 getStatus(),发现 pending 激增则调大并发或分段发。

十一、FAQ

  • 问:ApiResult<T> 有哪些字段?如何判断成功?

    • 答:本工程中统一通过 result.success 判断;成功时 result.data 可用;失败时 result.message/result.error/result.code 可读。保持上层只面向 ApiResult 即可。
  • 问:如何拿到任务 id 做单个取消?

    • 答:管理器内部会托管任务 id 并在完成时返回结果。实际工程中更推荐使用 tag 做页面/批量级别取消,更贴合使用场景。
  • 问:优先级会“饿死”低优先级吗?

    • 答:在极端高并发且持续有高优先级涌入时可能出现。可用“配额/分批”策略或限制高优先级占比来平衡。

十二、结语

  • 通过任务化抽象、优先级队列、并发控制、指数退避、自定义判定和统一取消,RequestStrategyManager 把“不可控的网络”收束成“可治理的系统”。
  • 你可以从“为页面统一打 tag + 配置合理重试 + 设置并发上限”三件事开始,立刻提升稳定性与可维护性。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容