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

4.8 KiB
Raw Blame History

【Demo】接口 + Isar + watch 完整链路(答错记错题)

源文件:lib/pages/demo/wrong_flow_demo.dart | 分类:存储 一句话:回答"现在不都调接口吗?" —— 接口、本地库、响应式三层协作的真实业务链路。


一、这个 Demo 解决什么问题

答错一题,要同时完成三件事:

  1. 接口上报(远端):服务端记录错题 —— 可能失败、可能慢;
  2. 本地落库(Isar):错题本快照,离线也能看;
  3. watch 响应式:本地库一变,错题列表自动刷新。
流程:答错 → ① 上报接口(失败也继续)→ ② 写本地错题本 → ③ watch 自动推送 → 列表自动更新

二、怎么运行 / 演示内容

runApp(const MaterialApp(home: WrongFlowDemoPage()));
  • 三道内置题,答错 → 完整链路日志(接口 → 本地 → watch);
  • "模拟断网"按钮:跳过接口,只写本地(演示离线兜底);
  • 下方错题本列表:watch 订阅,零手动刷新。

⚠ 依赖 Isar(native only),Web 端降级提示。


三、核心知识点(新手向)

1. 为什么接口失败本地仍记录(离线兜底)

学生端弱网是常态。错题是"本地刚需数据"——先落库先能用; 联网后再增量同步回服务端(真实项目用 sync 队列,错题加 isSynced 字段标记, 本例为演示未做,代码里有注释)。

// ① 上报接口:失败也继续(本地兜底)
await _reportToServer(wrongAnswer: _options[index]);
// ② 写本地错题本:watch 自动推送 → 下方列表自动更新
await WrongQuestionStore.instance.add(
  questionId: 100 + _qIndex,
  title: _question,
  wrongAnswer: _options[index],
);

2. 别把本地写入放接口 onError 里!

try {
  await DioClient.instance.post('/posts', data: {...});
  _appendLog('📡 接口上报成功');
} on ApiException catch (e) {
  // 接口失败不阻断:本地仍要记,联网后再同步
  _appendLog('📡 接口上报失败:${e.message}');
  _appendLog('   ↳ 本地仍记录,联网后增量同步(sync 队列)');
} finally {
  if (mounted) setState(() => _reporting = false);
}

独立的两步:上报和落库互不依赖。 如果写在 onError 里,就成了"只有失败才记录"——在线答错的题反而不进错题本,逻辑就反了。

3. 对比"纯接口 + 手动刷新"

不 watch 就得:答完错题 → 手动通知错题 tab 刷新(事件总线/页面重建)。 watch 让 UI 直接订阅数据库,写入和展示彻底解耦。

4. Dart 3 记录类型题库

// 题库:(题干, 选项, 正确答案索引)
static const List<(String, List<String>, int)> _bank = [
  ('7 × 8 = ?', ['54', '56', '58', '63'], 1),
  ('Flutter 官方推荐的状态管理?', ['Provider', 'Riverpod', 'BLoC', 'GetX'], 1),
];

String get _question => _bank[_qIndex].$1;   // 题干
List<String> get _options => _bank[_qIndex].$2; // 选项
int get _rightIndex => _bank[_qIndex].$3;      // 正确答案索引

四、关键代码剖析

// 答题:对 → 不记录;错 → 接口上报 + 本地错题(watch 自动刷新)
Future<void> _onPick(int index) async {
  if (_reporting) return; // 上报中防重入
  final isRight = index == _rightIndex;
  setState(() { _selected = index; _isRight = isRight; });

  if (isRight) {
    _appendLog('✅ 回答正确,不记错题');
  } else {
    _appendLog('❌ 答错「${_question}」(选了 ${_options[index]}),进入链路...');
    await _reportToServer(wrongAnswer: _options[index]); // ① 上报
    await WrongQuestionStore.instance.add(...);          // ② 落库
    _appendLog('💾 已写入本地错题本 → watch 自动刷新列表');
  }

  if (!mounted) return;
  setState(() { _qIndex = (_qIndex + 1) % _bank.length; _selected = null; _isRight = null; });
}

五、踩坑记录

  1. watch 只在 native 可用(web 空实现),本页 kIsWeb 降级提示;
  2. 写库必须在 writeTxn 内(store 已封装);
  3. 接口上报和本地写入是两个独立步骤,别把本地写入放接口 onError 里;
  4. 上报中要防重入(_reporting 禁用选项点击),否则连点会重复上报;
  5. 异步(await)之后 setState 前判 mounted。

六、扩展思考

  • sync 队列进阶:错题表加 isSynced 字段 → 启动/联网时查 isSynced == false 批量补报 → 成功后置位;
  • 与 isar_watch_demo.md 对照:一个讲"watch 本身",一个讲"完整业务链路";
  • 上报失败的重试策略可复用 DioClient 的指数退避(见 dio_pro_demo.md);
  • 真实项目:本页的 _bank 换成接口拉取的题,WrongQuestionStore.add 换成带 userId 的写入。