4.6 KiB
4.6 KiB
【Demo】Isar watch 响应式(错题本)
源文件:
lib/pages/demo/isar_watch_demo.dart| 分类:存储 一句话:主动查库是"浅水区",watch()订阅数据库是"深水区" —— 数据一变列表自动刷新。
一、这个 Demo 解决什么问题
本地数据库(Isar)的基本用法是 findAll() 主动查库:数据变了要自己再查/刷新。
但页面要的是"数据库一变,UI 自动更新"——这就是 watch 响应式:
watch() 订阅数据库,写入/删除事件自动推送,配合 StreamBuilder 自动重建列表。
对照前端:watch ≈ 前端框架的"数据源订阅"——UI 直接订阅数据库, 写入和展示彻底解耦,不用手动通知刷新。
二、怎么运行 / 演示内容
runApp(const MaterialApp(home: IsarWatchDemoPage()));
- 上方:输入题干 → "加入错题"(写入 Isar);
- 下方:
watchAll()订阅的列表 无需任何刷新 自动更新(fireImmediately首帧先出当前数据); - 对比区:一个"手动查询"按钮模拟浅水区做法(要自己点才更新);
- 点列表项 → 删除 → watch 同样自动收到删除事件。
⚠ Isar 仅 native 可用:Web 端本页显示降级提示,请用 Android/iOS/桌面端看效果。
三、核心知识点(新手向)
1. watch 的用法
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 访问字段
// item 是 WrongQuestion 对象(页面不 import 模型,用 dynamic 访问)
final title = item.title as String? ?? '无题干';
final wrongAt = item.wrongAt as DateTime?;
这样页面层与 Isar 模型层彻底解耦,web 编译也不受影响。
4. 写入必须 writeTxn
await WrongQuestionStore.instance.add(
questionId: DateTime.now().millisecondsSinceEpoch % 100000,
title: title,
wrongAnswer: '选错了',
);
写操作必须 writeTxn 包住(store 内部已封装),页面不用管。
四、关键代码剖析
// 添加错题(写入 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);
}
五、踩坑记录
- watch 返回的是 Stream,必须配 StreamBuilder(或手动 cancel),否则订阅泄漏;
fireImmediately: false时首帧为空 —— 列表会闪一下"空",通常要开true;- 写操作必须 writeTxn 包住(store 已处理,绕过 store 裸写会报错);
- Web 端无法跑 Isar(条件导出空实现),只能真机/桌面看效果;
dynamic访问字段要as强转或判空,别直接.title(分析器不报错,运行期可能崩)。
六、扩展思考
- 完整链路(接口 + Isar + watch)见 wrong_flow_demo.md;
- 条件导出 + 三件套(io/web)是本项目 web 兼容的核心手段,见架构文档"零、新手前置";
- Isar 还有
watchLazy()(只通知不传数据)、watchObject(id)(单对象订阅); - 进阶:
filter + sortBy + offset/limit组合查询,配合 watch 做"分页错题本"。