11、Flow与Room:Room的Flow支持、观察数据库变化、结合Flow进行数据驱动UI更新
说实话,Room 和 Flow 的搭配,是我在 Android 开发中最喜欢的组合之一。你想想看,数据库里的数据变了,UI 自动就跟着更新——这不就是我们梦寐以求的响应式编程吗?
我个人习惯把这种模式叫做「数据管道」。数据从 Room 流出来,经过各种变换,最后直接灌进 UI。整个过程不需要你手动刷新,不需要写一堆回调监听器。嗯,这才是现代 Android 开发该有的样子。
Room 对 Flow 的原生支持
Room 从 2.1 版本开始就支持 Flow 了。说白了,你只需要在 DAO 方法里把返回类型改成 Flow,Room 就会自动帮你做两件事:
- 查询数据并发射
- 监听表变化,数据变了就重新发射
来看个最简单的例子:
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Long): Flow<User?>
}
就这么简单。每次 users 表有增删改操作,Room 就会自动重新执行查询,然后把新结果发射出来。我在项目中遇到过有人问:「那如果我只改了某一行,Room 会重新查整个表吗?」答案是会的。Room 的机制是监听表级别的变化,不是行级别的。
@Transaction 批量更新,减少触发次数。
观察数据库变化的底层原理
Room 是怎么知道数据变了呢?其实它内部用了一个叫 InvalidationTracker 的东西。每次你执行写操作(insert、update、delete),Room 都会记录下哪些表被修改了。然后它会通知所有监听这些表的 Flow 订阅者:「嘿,你的数据可能过期了,重新查一下吧。」
我画了一张图,帮你理解这个流程:
这个机制的好处是——你完全不用操心「什么时候该刷新数据」。Room 帮你盯着呢。但坏处是——如果你不小心,可能会触发不必要的查询。比如你只是更新了一个不相关的字段,Room 也会通知所有监听者。
结合 Flow 进行数据驱动 UI 更新
好了,现在数据库能自动通知变化了。那怎么跟 UI 联动呢?我一般用 collect 或者 collectAsState(如果是 Compose)。
先看个 ViewModel 层的典型写法:
class UserViewModel(private val userDao: UserDao) : ViewModel() {
// 直接暴露 Flow,让 UI 层去 collect
val users: Flow<List<User>> = userDao.getAllUsers()
.map { list ->
// 可以做数据转换
list.filter { it.isActive }
}
.catch { e ->
// 异常处理
emit(emptyList())
}
// 或者用 StateFlow 做状态管理
private val _searchQuery = MutableStateFlow("")
val searchResults: StateFlow<List<User>> = _searchQuery
.debounce(300)
.flatMapLatest { query ->
userDao.searchUsers("%$query%")
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun updateSearchQuery(query: String) {
_searchQuery.value = query
}
}
这里有几个关键点我想强调一下:
- flatMapLatest:每次搜索词变了,它会取消上一个查询,只保留最新的。避免并发问题。
- debounce(300):用户打字时不会每输入一个字符就查一次,等 300 毫秒没新输入再查。用户体验好很多。
- stateIn:把 Flow 转成 StateFlow,方便 UI 层直接读取当前值。
再看 UI 层怎么用。如果你用的是 Compose:
@Composable
fun UserListScreen(viewModel: UserViewModel) {
val users by viewModel.users.collectAsState(initial = emptyList())
LazyColumn {
items(users) { user ->
UserItem(user = user)
}
}
}
如果你用的是传统 View 系统:
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.users.collect { users ->
adapter.submitList(users)
}
}
}
}
看到没?代码量少得可怜。没有手动刷新,没有监听器注册注销,没有 CursorLoader。就是 collect 一下,完事。
避坑指南:我曾经踩过的坑
讲几个我实际遇到过的问题,希望能帮你少走弯路:
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| Flow 不发射数据 | UI 一直空白 | Room 的 Flow 查询在表为空时不会发射 null,而是发射空列表。但如果你用了 .first() 可能会阻塞 |
用 .catch { emit(emptyList()) } 兜底 |
| UI 频繁刷新 | 列表闪烁 | 每次表变化都重新查询,即使数据没变 | 用 distinctUntilChanged() 过滤重复数据 |
| 内存泄漏 | Activity 销毁后还在查询 | Flow 没有在合适的生命周期停止 | 用 lifecycleScope 或 repeatOnLifecycle 管理 |
我曾经在一个项目里,因为忘了加 distinctUntilChanged(),导致每次数据库有微小变化(比如时间戳更新)整个列表都重新渲染。用户反馈说「滑动列表时卡顿明显」。后来加上这个操作符,问题立刻解决了。
核心要点总结:
- Room + Flow = 自动监听数据库变化,无需手动刷新
- 用 flatMapLatest 处理搜索等动态查询,避免并发问题
- 用 stateIn 配合 WhileSubscribed 优化生命周期管理
- 别忘了 distinctUntilChanged 过滤重复数据
- 异常处理用 catch 操作符,别让崩溃传播到 UI 层
说实话,Room 和 Flow 的配合,是我觉得 Android 开发中最优雅的数据层方案之一。你不需要引入 RxJava 那样的重型框架,也不需要自己写一堆观察者模式。Kotlin 协程 + Flow + Room,三剑客搞定一切。
嗯,今天就聊到这里。记住一点:数据驱动 UI 的核心思想,就是让数据的变化自动流向 UI,而不是你去「拉」数据。Room 的 Flow 支持,正是这个思想的最佳实践。
公众号:蓝海资料掘金营,微信deep3321