Files
App.K12.Student.Miniapp_Flu…/docs/demos/dio_demo.md
T
2026-09-04 15:22:20 +08:00

130 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【Demo】Dio 网络请求(封装 + 三态 UI)
> 源文件:`lib/pages/demo/dio_demo.dart` | 分类:网络
> 一句话:GET/POST/报错一次跑通,学"统一请求入口 + 加载/成功/失败三态 UI"的页面范式。
---
## 一、这个 Demo 解决什么问题
新手第一次接接口时的完整范式:
1. 请求期间要显示 loading、按钮要禁用(防重复点击);
2. 错误要统一翻译成中文提示(封装层做,页面不碰底层细节);
3. **await 之后 setState 前必须判 mounted**(用户可能已切走页面)。
---
## 二、怎么运行 / 演示内容
```dart
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(页面范式)
```dart
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
```dart
// 加载中:转圈
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. 按钮防重
```dart
FilledButton.icon(
onPressed: _loading ? null : _fetchOne, // loading 时禁用
...
)
```
---
## 四、关键代码剖析
```dart
/// 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}';
});
});
}
```
---
## 五、踩坑记录
1. **await 之后 setState 前必须 `if (!mounted) return`** —— 异步期间页面可能已销毁,直接 setState 会报错;
2. **catch 顺序**:先 `on ApiException`(业务错误),再 `catch (e)`(兜底未知异常);
3. 别在页面里直接 catch `DioException` —— 封装层的翻译就白做了;
4. loading 复位放 `finally`,不是 try 里 —— 否则异常路径永远转圈;
5. 真实项目 URL 用相对路径(baseUrl 在 DioClient 统一配),别在页面写死域名。
---
## 六、扩展思考
- 生产级能力(取消/进度/重试)见 [dio_pro_demo.md](dio_pro_demo.md);
- 上传文件(FormData + onSendProgress)见 [oss_upload_demo.md](oss_upload_demo.md);
- 页面更进阶的写法:用 Riverpod `AsyncNotifierProvider` 管理请求状态,AsyncValue 自带 loading/error/data 三态(见 exam 架构文档);
- 错误重试策略:`DioClient` 已内置指数退避重试(只对网络层错误),开关见 dio_pro_demo。