4.1 KiB
4.1 KiB
【Demo】Dio 网络请求(封装 + 三态 UI)
源文件:
lib/pages/demo/dio_demo.dart| 分类:网络 一句话:GET/POST/报错一次跑通,学"统一请求入口 + 加载/成功/失败三态 UI"的页面范式。
一、这个 Demo 解决什么问题
新手第一次接接口时的完整范式:
- 请求期间要显示 loading、按钮要禁用(防重复点击);
- 错误要统一翻译成中文提示(封装层做,页面不碰底层细节);
- await 之后 setState 前必须判 mounted(用户可能已切走页面)。
二、怎么运行 / 演示内容
runApp(const MaterialApp(home: DioDemoPage()));
4 个按钮:
- GET 单条:
PostApi.fetchPost(1); - GET 列表:
PostApi.fetchPosts(userId: 1)(jsonplaceholder mock); - POST 新建:
PostApi.createPost(...); - 故意报错:请求不存在的路径,看统一错误提示。
请求/响应日志在控制台打印(DioClient 拦截器里 debugPrint 完整 URL)。
三、核心知识点(新手向)
1. 项目的网络分层(重点!)
页面(本 Demo)→ PostApi(业务接口,只定义"有哪些接口")
→ DioClient(封装层:baseUrl/超时/拦截器/错误翻译)
→ Dio(底层库)
PostApi只关心业务:fetchPost(1)、fetchPosts(userId: 1);DioClient统一处理:日志、超时、把DioException翻译成中文ApiException;- 页面 只 catch
ApiException,不用关心 Dio 的细节类型。
2. 统一请求入口 _run(页面范式)
Future<void> _run(Future<void> Function() action) async {
setState(() {
_loading = true;
_error = null;
});
try {
await action();
} on ApiException catch (e) {
if (!mounted) return;
setState(() => _error = '请求失败:${e.message}');
} catch (e) {
if (!mounted) return;
setState(() => _error = '未知异常:$e');
} finally {
if (mounted) setState(() => _loading = false); // 无论成败都复位
}
}
finally里复位_loading:成功/失败都要恢复,忘记写就会永远转圈;- 每个具体请求只是往
_run里传一个闭包,loading 逻辑只写一遍。
3. 三态 UI
// 加载中:转圈
if (_loading) Center(child: CircularProgressIndicator());
// 失败:红底错误卡
if (_error != null)
Container(
color: const Color(0xFFFDECEA),
child: Text(_error!, style: const TextStyle(color: Color(0xFFD32F2F))),
);
// 成功:结果卡(SelectableText 可复制)
Container(
child: SelectableText(_resultBody.isEmpty ? '(暂无数据)' : _resultBody),
)
4. 按钮防重
FilledButton.icon(
onPressed: _loading ? null : _fetchOne, // loading 时禁用
...
)
四、关键代码剖析
/// GET 单条
Future<void> _fetchOne() async {
await _run(() async {
final post = await PostApi.fetchPost(1);
if (!mounted) return;
setState(() {
_resultTitle = 'GET /posts/1 成功';
_resultBody = 'id: ${post.id}\nuserId: ${post.userId}\ntitle: ${post.title}';
});
});
}
五、踩坑记录
- await 之后 setState 前必须
if (!mounted) return—— 异步期间页面可能已销毁,直接 setState 会报错; - catch 顺序:先
on ApiException(业务错误),再catch (e)(兜底未知异常); - 别在页面里直接 catch
DioException—— 封装层的翻译就白做了; - loading 复位放
finally,不是 try 里 —— 否则异常路径永远转圈; - 真实项目 URL 用相对路径(baseUrl 在 DioClient 统一配),别在页面写死域名。
六、扩展思考
- 生产级能力(取消/进度/重试)见 dio_pro_demo.md;
- 上传文件(FormData + onSendProgress)见 oss_upload_demo.md;
- 页面更进阶的写法:用 Riverpod
AsyncNotifierProvider管理请求状态,AsyncValue 自带 loading/error/data 三态(见 exam 架构文档); - 错误重试策略:
DioClient已内置指数退避重试(只对网络层错误),开关见 dio_pro_demo。