Room 返回 Flow 为什么能自动更新?
Room DAO 返回 Flow 时,数据表变化后 UI 能自动收到新数据。这背后不是 Flow 自己感知数据库,而是 Room 把数据库变更通知接进了 Flow。
一句话概括链路:Flow 被 collect 时执行查询;Room 通过 InvalidationTracker 观察相关表;表变化后触发 invalidation;Room 重新查询,并向 Flow 发射新结果。
Flow 本身不会监听数据库
Kotlin Flow 是异步数据流抽象,它知道怎么发射数据、取消、背压和切换上下文,但它不知道 SQLite 表什么时候变化。自动更新能力来自 Room 生成的 DAO 实现。
当你写下这样的 DAO:
@Query("SELECT * FROM user ORDER BY updatedAt DESC")
fun observeUsers(): Flow<List<User>>
Room 在编译期会通过 KSP/KAPT 生成实现代码。这个实现不只是执行一次 SQL,而是把查询和表观察绑定起来。collect 开始时执行初始查询;相关表变化时,再次调度查询。
InvalidationTracker 做了什么
Room 的 InvalidationTracker 负责记录哪些表被观察,以及这些表何时发生变化。对于 DAO 查询,Room 能从 SQL 中分析出相关表,例如 user、message、conversation。
当数据库事务提交后,如果相关表发生写入,InvalidationTracker 会通知观察者。Flow 收到通知后不会把变更行直接发出去,而是重新执行整个查询,把新查询结果发射给 collector。
这点很关键:Room Flow 的自动更新是“表失效后重新查询”,不是“增量 patch 一行数据”。所以查询越重,表变化越频繁,重新查询成本越需要关注。
为什么事务很重要
如果一次业务操作要写多张表,应该放在事务里。否则 UI 可能收到中间状态:第一张表写完触发一次 invalidation,第二张表写完又触发一次 invalidation,列表可能短暂展示不一致数据。
事务提交后统一触发通知,能减少重复查询,也能保证 UI 看到的数据更接近业务一致状态。
对于 Paging3 + Room 的场景尤其重要。RemoteMediator 写入列表数据和 remote keys 时,如果不在同一事务里,崩溃或取消后可能出现列表数据和分页 key 不一致,后续分页就会错乱。
Flow 的冷流特性
DAO 返回的 Flow 通常是冷流。没有 collector 时,它不会持续查询数据库;有 collector 时,它才注册观察并执行查询。多个 collector 可能触发多份查询,具体取决于你是否在上层用 stateIn、shareIn 共享。
在 ViewModel 中,如果多个 UI 组件都需要同一份数据库数据,可以考虑把 DAO Flow 转成 StateFlow:
val users = dao.observeUsers()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
这样 UI 旋转或短暂重订阅时,不必每个 collector 都从头组织一套状态。是否共享要看数据量和页面结构,不是所有 DAO Flow 都必须 stateIn。
自动更新的常见误区
第一个误区是以为任何数据库变化都会触发。Room 只会观察查询相关表。你更新了无关表,当前 Flow 不会重新发射。
第二个误区是以为结果相同就不会发射。Room 会在 invalidation 后重新查询,是否去重取决于上层是否使用 distinctUntilChanged(),以及实体 equals 是否合理。
第三个误区是把复杂 join 放进高频更新表上。只要其中一张相关表变化,整条查询就可能重新执行。复杂统计、聚合、join 要关注索引和查询成本。
第四个误区是在主线程处理大结果。Room 查询本身可以调度到后台,但发射到 UI 后,如果你在 map 里做大量转换,仍然可能卡主线程。重转换应该放在合适的 dispatcher 或提前建模。
什么时候需要手动刷新
Room Flow 解决的是本地数据库变化通知,不解决网络同步。服务端数据变了,数据库不知道,除非你有同步任务把新数据写入本地。因此常见架构是 Repository 暴露 Room Flow,同时由刷新逻辑负责拉取网络并写库。
UI 观察 Flow 获取本地最新状态;下拉刷新、定时同步、RemoteMediator 或后台任务负责更新数据库。这样本地和远程职责清晰,UI 不需要关心数据来自网络还是缓存。
结论
Room Flow 自动更新的本质是 Room 生成代码 + InvalidationTracker + 重新查询。Flow 提供异步流模型,Room 提供数据库失效通知,两者组合后才形成响应式数据库访问。
理解这条链路后,很多问题都能解释:为什么写库后 UI 更新,为什么复杂查询会频繁执行,为什么事务能减少中间状态,为什么网络变化不会自动进入 Flow。