4.8 KiB
4.8 KiB
【Demo】接口 + Isar + watch 完整链路(答错记错题)
源文件:
lib/pages/demo/wrong_flow_demo.dart| 分类:存储 一句话:回答"现在不都调接口吗?" —— 接口、本地库、响应式三层协作的真实业务链路。
一、这个 Demo 解决什么问题
答错一题,要同时完成三件事:
- 接口上报(远端):服务端记录错题 —— 可能失败、可能慢;
- 本地落库(Isar):错题本快照,离线也能看;
- 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; });
}
五、踩坑记录
- watch 只在 native 可用(web 空实现),本页
kIsWeb降级提示; - 写库必须在 writeTxn 内(store 已封装);
- 接口上报和本地写入是两个独立步骤,别把本地写入放接口 onError 里;
- 上报中要防重入(
_reporting禁用选项点击),否则连点会重复上报; - 异步(await)之后 setState 前判 mounted。
六、扩展思考
- sync 队列进阶:错题表加
isSynced字段 → 启动/联网时查isSynced == false批量补报 → 成功后置位; - 与 isar_watch_demo.md 对照:一个讲"watch 本身",一个讲"完整业务链路";
- 上报失败的重试策略可复用
DioClient的指数退避(见 dio_pro_demo.md); - 真实项目:本页的
_bank换成接口拉取的题,WrongQuestionStore.add换成带 userId 的写入。