# 【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>( // 响应式核心:数据库变化自动推送;fireImmediately 首帧先出一份当前数据 stream: WrongQuestionStore.instance.watchAll(), builder: (context, snapshot) { if (snapshot.hasError) return const Text('监听出错'); final items = snapshot.data ?? const []; // items 变化即重建列表 }, ) ``` - `watchAll()` 返回 `Stream`,页面必须用 `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 _addWrong() async { final title = _titleC.text.trim(); if (title.isEmpty) { /* SnackBar 提示 */ return; } await WrongQuestionStore.instance.add(...); _titleC.clear(); // 清空输入框(controller 直改,无需 setState) } // 删除(watch 也会收到删除事件,列表自动减少) Future _delete(dynamic item) async { final id = item.id as int; await WrongQuestionStore.instance.remove(id); } // 手动查询(对比:非响应式,必须自己点才更新) Future _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 做"分页错题本"。