flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+128
View File
@@ -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 的写入。