flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
# 【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。