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

4.1 KiB
Raw Blame History

【Demo】Dio 网络请求(封装 + 三态 UI)

源文件:lib/pages/demo/dio_demo.dart | 分类:网络 一句话:GET/POST/报错一次跑通,学"统一请求入口 + 加载/成功/失败三态 UI"的页面范式。


一、这个 Demo 解决什么问题

新手第一次接接口时的完整范式:

  1. 请求期间要显示 loading、按钮要禁用(防重复点击);
  2. 错误要统一翻译成中文提示(封装层做,页面不碰底层细节);
  3. 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}';
    });
  });
}

五、踩坑记录

  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;
  • 上传文件(FormData + onSendProgress)见 oss_upload_demo.md;
  • 页面更进阶的写法:用 Riverpod AsyncNotifierProvider 管理请求状态,AsyncValue 自带 loading/error/data 三态(见 exam 架构文档);
  • 错误重试策略:DioClient 已内置指数退避重试(只对网络层错误),开关见 dio_pro_demo。