5.1 网络请求落地:http 与 dio 双方案


5.1 网络请求落地:http 与 dio 双方案

本节摘要:网络请求是数据进出的第一道门。本节先用官方 http 包打通最短路径,再换 dio 搭起拦截器体系——统一超时、鉴权、错误归一化与重试,全部围绕轻记账的账单同步接口展开。读完你将拥有一套可直接搬走的生产级网络层骨架。

最短路径:http 包的三步走

Flutter 官方的 http 包把请求压缩成三步:建客户端、发请求、收响应。轻记账拉取当月账单的最短实现:

import 'dart:convert'; import 'package:http/http.dart' as http; Future<List<Map<String, Object?>>> fetchBillsRaw(String token) async { final resp = await http .get( Uri.https('api.lightledger.example', '/v1/bills', {'month': '2026-09'}), headers: {'Authorization': 'Bearer $token'}, ) .timeout(const Duration(seconds: 10)); // 超时必须有:默认无限等 if (resp.statusCode == 200) { return (jsonDecode(utf8.decode(resp.bodyBytes)) as List).cast<Map<String, Object?>>(); } throw HttpException('拉取失败:状态码 ${resp.statusCode}'); }

三处细节是新手坑的高发区:超时要显式设,http 默认不超时,弱网下界面会"永远转圈";解码先过 utf8,中文报文不处理编码会出现乱码;状态码要自查,http 包不会把 4xx、5xx 自动变成异常,200 之外全靠自己判断。这个最短路径适合请求量少、逻辑简单的场景——但随着接口多起来,每处都要重复写超时、鉴权、状态码检查,重复本身就是危险信号。

选型判断:什么时候换 dio

两个包的关系不是新旧替代,而是"裸工具"与"工地体系"的差别。判断标准一句话:需要"每个请求都共享的横切逻辑"(鉴权、日志、重试、错误归一化)时,换 dio;偶尔发两个请求,http 直用更轻。

维度 http dio
定位 最小官方库 全功能网络客户端
拦截器 无,逻辑散在调用处 请求与响应两级拦截器
超时 手动 .timeout 每处补 客户端级统一配置
错误形态 状态码自查 异常类型丰富,可分类处理
取消请求 支持但繁琐 CancelToken 一等公民

轻记账选 dio,理由落在拦截器:鉴权令牌的刷新、请求日志、错误统一转成应用内异常,这三件事在 http 方案里要写 N 遍,在 dio 里写一遍。

图 12 dio 网络层的分层与拦截器位置

图 12 dio 网络层的分层与拦截器位置

动手:生产级的 dio 客户端

下面是轻记账网络层的完整骨架,每一层都对应真实线上问题:

import 'package:dio/dio.dart'; class LedgerApiClient { LedgerApiClient({required String Function() tokenProvider}) : _tokenProvider = tokenProvider { _dio = Dio(BaseOptions( connectTimeout: const Duration(seconds: 8), receiveTimeout: const Duration(seconds: 15), // 大报文给足收包时间 baseUrl: 'https://api.lightledger.example/v1/', )); _dio.interceptors.addAll([ InterceptorsWrapper( onRequest: (options, handler) { options.headers['Authorization'] = 'Bearer ${_tokenProvider()}'; handler.next(options); }, onError: (e, handler) async { // 令牌过期:刷新后原请求重放一次,调用方无感 if (e.response?.statusCode == 401) { await refreshToken(); final retry = await _dio.fetch(e.requestOptions..headers['Authorization'] = 'Bearer ${_tokenProvider()}'); return handler.resolve(retry); } handler.next(e); }, ), LogInterceptor(requestBody: false, responseBody: true), // 上线前关掉 ]); } final String Function() _tokenProvider; late final Dio _dio; Future<List<RemoteBill>> fetchBills(String month) async { try { final resp = await _dio.get<List<dynamic>>('bills', queryParameters: {'month': month}); return (resp.data ?? []).map(RemoteBill.fromMap).toList(); } on DioException catch (e) { throw _normalize(e); // 调用方只见应用异常,不见网络库细节 } } AppNetworkException _normalize(DioException e) { return switch (e.type) { DioExceptionType.connectionTimeout || DioExceptionType.receiveTimeout => const AppNetworkException.offline(), DioExceptionType.badResponse => AppNetworkException.server(e.response?.statusCode), _ => const AppNetworkException.unknown(), }; } }

设计意图逐层拆:超时分级(连接短、收行长——拉整月账单与拉配置的合理等待不同);鉴权进拦截器(调用方永远不碰令牌,401 自动刷新重放是移动应用体验的分水岭——用户一辈子不该看到"登录过期"弹窗,除非刷新真失败了);错误归一化(控制器拿到的是"离线、服务端错误、未知"三种应用语义,switch 表达式穷举处理,第 1 章的模式匹配在这里收利息)。

失败是常态:调用侧的正确姿势

网络层立好后,调用侧的纪律只有一条:每个请求都按会失败来写。轻记账同步入口的完整处理:

Future<void> syncNow(WidgetRef ref) async { ref.read(syncStateProvider.notifier).set(Syncing()); try { await ref.read(syncUseCaseProvider).run(); ref.read(syncStateProvider.notifier).set(SyncDone()); } on AppNetworkException catch (e) { // 离线不弹窗打扰:状态条提示即可,队列里还有第 5.3 节的补传兜底 ref.read(syncStateProvider.notifier).set(SyncFailed(e.kind)); } }

注意"离线不弹窗"的产品判断:移动网络的失败率让弹窗变成骚扰,把失败降级为状态条,再靠离线队列兜底——技术方案与产品判断在这里合流,这正是"网络层工程"区别于"调 API"的地方。

本节要点回顾

  • http 三步走适合轻量场景,超时、编码、状态码三件事必须自己做;
  • 需要横切逻辑(鉴权、日志、重试、归一化)时换 dio,拦截器写一遍管所有请求;
  • 超时分级:连接超时短、收发超时按报文体量给;
  • 401 自动刷新重放做进拦截器,调用方与用户都无感;
  • 调用侧按会失败来写,离线降级为状态提示而非弹窗,队列兜底交给下一节。

请求发出去了、错误也兜住了,但报文还是一坨 Map。下一节把它翻译成类型安全的 Dart 对象。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U