flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# 【Demo】Dio 生产级能力(进度 / 取消 / 重试)
|
||||
|
||||
> 源文件:`lib/pages/demo/dio_pro_demo.dart` | 分类:网络
|
||||
> 一句话:下载进度条、页面销毁自动取消请求、网络错误自动重试 —— 生产环境三件套。
|
||||
|
||||
---
|
||||
|
||||
## 一、这个 Demo 解决什么问题
|
||||
|
||||
[dio_demo.md](dio_demo.md) 讲了基础请求,本页升级到生产级场景:
|
||||
1. **下载进度**:`onReceiveProgress` 实时百分比 + 进度条;
|
||||
2. **请求取消**:用户手动取消 + **页面销毁自动取消**(防泄漏);
|
||||
3. **失败重试**:网络层错误自动指数退避重试 N 次(控制台看 ↻ 日志)。
|
||||
|
||||
---
|
||||
|
||||
## 二、怎么运行 / 演示内容
|
||||
|
||||
```dart
|
||||
runApp(const MaterialApp(home: DioProDemoPage()));
|
||||
```
|
||||
|
||||
- "开始下载":下载约 2MB 的视频文件,进度条动起来;
|
||||
- "取消下载":手动中断;
|
||||
- "重试演示":请求不存在的域名 → DNS 解析失败 → 自动重试 3 次(指数退避)→ 抛"网络异常";
|
||||
- 顶部开关控制是否启用重试,方便对比。
|
||||
|
||||
---
|
||||
|
||||
## 三、核心知识点(新手向)
|
||||
|
||||
### 1. 为什么需要 CancelToken
|
||||
页面销毁但请求还在跑 → Future 没人接,响应回调还可能在 setState(对已销毁的 State setState 会报错/泄漏)。
|
||||
|
||||
做法:
|
||||
```dart
|
||||
CancelToken? _token;
|
||||
|
||||
Future<void> _startDownload() async {
|
||||
final token = CancelToken(); // 每个请求新建一个(取消后不可复用!)
|
||||
_token = token;
|
||||
try {
|
||||
await DioClient.instance.get(
|
||||
_bigUrl,
|
||||
cancelToken: token,
|
||||
onReceiveProgress: (received, total) {
|
||||
if (!mounted || token.isCancelled) return;
|
||||
setState(() => _progress = total <= 0 ? 0 : received / total);
|
||||
},
|
||||
retries: _retryEnabled ? 2 : 0,
|
||||
);
|
||||
} on ApiException catch (e) {
|
||||
// 取消也会走到这里("请求已取消"),统一复位 UI
|
||||
if (!mounted) return;
|
||||
setState(() { _log += '✖ ${e.message}\n'; _downloading = false; });
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_token?.cancel('页面已销毁'); // 页面销毁 → 自动取消在途请求(核心防泄漏点)
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 进度回调的三个坑
|
||||
```dart
|
||||
onReceiveProgress: (received, total) {
|
||||
if (!mounted || token.isCancelled) return; // ① 回调频率高,必判 mounted
|
||||
setState(() => _progress = total <= 0 ? 0 : received / total); // ② total<=0 除零兜底
|
||||
},
|
||||
```
|
||||
- `total <= 0`:响应没有 Content-Length 时 total 为 -1,直接除会算出负数进度;
|
||||
- 进度回调每秒触发很多次,每次 setState 前判 mounted 是性能与安全双重保险。
|
||||
|
||||
### 3. 重试为什么只对"网络层错误"生效
|
||||
| 错误类型 | 可重试? | 原因 |
|
||||
|---|---|---|
|
||||
| connectionError(断网/域名解析失败)、超时、闪断 | ✅ | 网络恢复后大概率成功 |
|
||||
| 主动取消(用户意图) | ❌ | 用户不要了还重试 = 反人性 |
|
||||
| badResponse(4xx/5xx) | ❌ | 重试大概率同样失败,POST 还会**重复提交数据** |
|
||||
|
||||
这是生产级的取舍,见 `dio_client.dart` 的 `_canRetry`。
|
||||
|
||||
### 4. Web 调试的 CORS 坑
|
||||
- `application/json` 是**非简单头**,跨域 GET 会先发 **OPTIONS 预检**,静态服务器不支持就 405 被拦;
|
||||
- 下载媒体/图片时用 headers 覆盖成 **text/plain 简单头**,浏览器不再预检直接放行(真机无此问题)。
|
||||
|
||||
```dart
|
||||
headers: const {'Content-Type': 'text/plain'}, // Web 调试必加!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、关键代码剖析
|
||||
|
||||
```dart
|
||||
// 手动取消
|
||||
void _cancelDownload() {
|
||||
_token?.cancel('用户点击取消');
|
||||
setState(() => _log += '⏹ 已发起取消,等待请求中断...\n');
|
||||
}
|
||||
```
|
||||
取消后请求抛 `DioExceptionType.cancel` → 封装层翻译成 `ApiException('请求已取消')` → 走 onError 分支。
|
||||
|
||||
```dart
|
||||
// 重试演示:Stopwatch 统计总耗时
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
await DioClient.instance.get('https://no-such-domain-abcxyz.com/api/test',
|
||||
retries: _retryEnabled ? 3 : 0);
|
||||
} on ApiException catch (e) {
|
||||
stopwatch.stop();
|
||||
setState(() => _log = '✖ ${e.message}\n⏱ 共耗时 ${stopwatch.elapsed.inMilliseconds}ms\n'
|
||||
'(重试开关:${_retryEnabled ? '开' : '关'})');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、踩坑记录
|
||||
|
||||
1. **`onReceiveProgress` 里 setState 前必须判 mounted**(回调频率高,页面销毁后必崩);
|
||||
2. **`total <= 0` 时进度除零**,要兜底成 0;
|
||||
3. **CancelToken 一旦取消就不能复用**,每次请求要新建;
|
||||
4. **取消走 onError 分支**(DioExceptionType.cancel),不是"不走回调"——UI 复位逻辑要放对地方;
|
||||
5. Web 跨域下载:Content-Type 改 text/plain 绕过预检;
|
||||
6. 上传进度同理:`onSendProgress: (sent, total)`,笔记图片上传的下一步就接它。
|
||||
|
||||
---
|
||||
|
||||
## 六、扩展思考
|
||||
|
||||
- 重试策略进阶:`RetryInterceptor` 的 retryableExtra / 重试次数按接口配置;
|
||||
- 下载文件落盘:`dio` 的 `download()` + 进度回调 + `path_provider` 存沙盒;
|
||||
- 上传场景:[oss_upload_demo.md](oss_upload_demo.md) 的 OSS 直传 PUT 就带 onSendProgress;
|
||||
- 页面级取消还可以用 `DisposeBag` 模式统一管理多个 token(本项目单 token 够用)。
|
||||
Reference in New Issue
Block a user