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

120 lines
4.6 KiB
Markdown
Raw 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/isar_watch_demo.dart` | 分类:存储
> 一句话:主动查库是"浅水区",`watch()` 订阅数据库是"深水区" —— 数据一变列表自动刷新。
---
## 一、这个 Demo 解决什么问题
本地数据库(Isar)的基本用法是 `findAll()` **主动查库**:数据变了要自己再查/刷新。
但页面要的是"数据库一变,UI 自动更新"——这就是 **watch 响应式**:
`watch()` 订阅数据库,写入/删除事件**自动推送**,配合 `StreamBuilder` 自动重建列表。
> 对照前端:watch ≈ 前端框架的"数据源订阅"——UI 直接订阅数据库,
> 写入和展示彻底解耦,不用手动通知刷新。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: IsarWatchDemoPage()));
```
- 上方:输入题干 → "加入错题"(写入 Isar);
- 下方:`watchAll()` 订阅的列表 **无需任何刷新** 自动更新(`fireImmediately` 首帧先出当前数据);
- 对比区:一个"手动查询"按钮模拟浅水区做法(要自己点才更新);
- 点列表项 → 删除 → watch 同样自动收到删除事件。
> ⚠ Isar 仅 native 可用:Web 端本页显示降级提示,请用 Android/iOS/桌面端看效果。
---
## 三、核心知识点(新手向)
### 1. watch 的用法
```dart
StreamBuilder<List<dynamic>>(
// 响应式核心:数据库变化自动推送;fireImmediately 首帧先出一份当前数据
stream: WrongQuestionStore.instance.watchAll(),
builder: (context, snapshot) {
if (snapshot.hasError) return const Text('监听出错');
final items = snapshot.data ?? const <dynamic>[];
// items 变化即重建列表
},
)
```
- `watchAll()` 返回 `Stream<List>`,页面必须用 `StreamBuilder` 消费(StreamBuilder 自动管理订阅/取消);
- **`fireImmediately: false` 时首帧是空的** —— 通常要开 `true`,首帧先出一份当前数据。
### 2. 为什么经 store 而不是直接用 AppDatabase
Isar 集合成员只在 native 编译,**web 直接引用会编译失败**。
`WrongQuestionStore` 做了**条件导出**(io 真实现 / web 空实现),
页面永远只面向 store,web 端用 `kIsWeb` 降级提示。
(原理详见 `docs/exam_architecture.md` 的 Isar 教程章节。)
### 3. 页面不 import 模型,用 dynamic 访问字段
```dart
// item 是 WrongQuestion 对象(页面不 import 模型,用 dynamic 访问)
final title = item.title as String? ?? '无题干';
final wrongAt = item.wrongAt as DateTime?;
```
这样页面层与 Isar 模型层彻底解耦,web 编译也不受影响。
### 4. 写入必须 writeTxn
```dart
await WrongQuestionStore.instance.add(
questionId: DateTime.now().millisecondsSinceEpoch % 100000,
title: title,
wrongAnswer: '选错了',
);
```
写操作必须 `writeTxn` 包住(store 内部已封装),页面不用管。
---
## 四、关键代码剖析
```dart
// 添加错题(写入 Isar → watch 自动推送 → 列表自动更新)
Future<void> _addWrong() async {
final title = _titleC.text.trim();
if (title.isEmpty) { /* SnackBar 提示 */ return; }
await WrongQuestionStore.instance.add(...);
_titleC.clear(); // 清空输入框(controller 直改,无需 setState)
}
// 删除(watch 也会收到删除事件,列表自动减少)
Future<void> _delete(dynamic item) async {
final id = item.id as int;
await WrongQuestionStore.instance.remove(id);
}
// 手动查询(对比:非响应式,必须自己点才更新)
Future<void> _manualQuery() async {
final all = await WrongQuestionStore.instance.findAll();
if (!mounted) return;
setState(() => _manualCount = all.length);
}
```
---
## 五、踩坑记录
1. **watch 返回的是 Stream,必须配 StreamBuilder**(或手动 cancel),否则订阅泄漏;
2. **`fireImmediately: false` 时首帧为空** —— 列表会闪一下"空",通常要开 `true`;
3. **写操作必须 writeTxn 包住**(store 已处理,绕过 store 裸写会报错);
4. **Web 端无法跑 Isar**(条件导出空实现),只能真机/桌面看效果;
5. `dynamic` 访问字段要 `as` 强转或判空,别直接 `.title`(分析器不报错,运行期可能崩)。
---
## 六、扩展思考
- 完整链路(接口 + Isar + watch)见 [wrong_flow_demo.md](wrong_flow_demo.md);
- 条件导出 + 三件套(io/web)是本项目 web 兼容的核心手段,见架构文档"零、新手前置";
- Isar 还有 `watchLazy()`(只通知不传数据)、`watchObject(id)`(单对象订阅);
- 进阶:`filter + sortBy + offset/limit` 组合查询,配合 watch 做"分页错题本"。