flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# 【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 的写入。
|
||||
Reference in New Issue
Block a user