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

4.6 KiB
Raw Blame History

【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);
}

五、踩坑记录

  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;
  • 条件导出 + 三件套(io/web)是本项目 web 兼容的核心手段,见架构文档"零、新手前置";
  • Isar 还有 watchLazy()(只通知不传数据)、watchObject(id)(单对象订阅);
  • 进阶:filter + sortBy + offset/limit 组合查询,配合 watch 做"分页错题本"。