# 【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 _run(Future 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 _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。