Files
2026-09-04 15:22:20 +08:00

129 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【Demo】接口 + Isar + watch 完整链路(答错记错题)
> 源文件:`lib/pages/demo/wrong_flow_demo.dart` | 分类:存储
> 一句话:回答"现在不都调接口吗?" —— 接口、本地库、响应式三层协作的真实业务链路。
---
## 一、这个 Demo 解决什么问题
答错一题,要同时完成三件事:
1. **接口上报**(远端):服务端记录错题 —— 可能失败、可能慢;
2. **本地落库**(Isar):错题本快照,**离线也能看**;
3. **watch 响应式**:本地库一变,错题列表自动刷新。
```
流程:答错 → ① 上报接口(失败也继续)→ ② 写本地错题本 → ③ watch 自动推送 → 列表自动更新
```
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: WrongFlowDemoPage()));
```
- 三道内置题,答错 → 完整链路日志(接口 → 本地 → watch);
- "模拟断网"按钮:跳过接口,只写本地(演示离线兜底);
- 下方错题本列表:watch 订阅,零手动刷新。
> ⚠ 依赖 Isar(native only),Web 端降级提示。
---
## 三、核心知识点(新手向)
### 1. 为什么接口失败本地仍记录(离线兜底)
学生端弱网是常态。**错题是"本地刚需数据"**——先落库先能用;
联网后再**增量同步**回服务端(真实项目用 sync 队列,错题加 `isSynced` 字段标记,
本例为演示未做,代码里有注释)。
```dart
// ① 上报接口:失败也继续(本地兜底)
await _reportToServer(wrongAnswer: _options[index]);
// ② 写本地错题本:watch 自动推送 → 下方列表自动更新
await WrongQuestionStore.instance.add(
questionId: 100 + _qIndex,
title: _question,
wrongAnswer: _options[index],
);
```
### 2. 别把本地写入放接口 onError 里!
```dart
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 记录类型题库
```dart
// 题库:(题干, 选项, 正确答案索引)
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; // 正确答案索引
```
---
## 四、关键代码剖析
```dart
// 答题:对 → 不记录;错 → 接口上报 + 本地错题(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](isar_watch_demo.md) 对照:一个讲"watch 本身",一个讲"完整业务链路";
- 上报失败的重试策略可复用 `DioClient` 的指数退避(见 [dio_pro_demo.md](dio_pro_demo.md));
- 真实项目:本页的 `_bank` 换成接口拉取的题,`WrongQuestionStore.add` 换成带 userId 的写入。