9、Flow与Room:Kotlin Flow简介、在DAO中返回Flow、collect数据流、与LiveData对比
各位同学,今天我们来聊聊 Room 和 Flow 的搭配。说实话,刚接触 Kotlin 协程那会儿,我也觉得 Flow 有点抽象。但用顺手之后,你会发现它和 Room 简直是天生一对。我个人习惯把 Flow 理解成「水管里的水」——数据源源不断流过来,你只需要在出水口接一杯就行。
Kotlin Flow 简介
Flow 是 Kotlin 协程库里的冷数据流。什么叫冷数据流?说白了,就是没人订阅的时候它不干活。你想想看,这跟 LiveData 那种「不管有没有观察者,数据变了就通知」的机制完全不同。
Flow 有三个核心要素:
- 生产者:负责发射数据,比如 Room DAO 里的查询方法
- 中间操作:可以对数据做变换,比如 map、filter
- 消费者:通过 collect 来接收数据
我在项目中遇到过一个问题:用 Flow 做网络请求时,如果忘记加 .flowOn(Dispatchers.IO),UI 线程就会被阻塞。嗯,这里要注意,Room 的 Flow 默认是在后台线程执行的,但如果你自己构建 Flow,一定要指定调度器。
核心要点:Flow 是冷数据流,每次 collect 都会重新执行生产者代码。Room 的 Flow 查询会在表数据变化时自动重新发射,这是它最强大的地方。
在DAO中返回Flow
在 Room 里返回 Flow 非常简单。你只需要把 DAO 方法的返回类型改成 Flow<T> 就行。来看代码:
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY id DESC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Int): Flow<User?>
}
你看,就这么简单。Room 会自动帮你做两件事:
- 首次查询时发射一次数据
- 当 users 表发生任何增删改操作时,自动重新查询并发射新数据
我曾经踩过一个坑:在 DAO 里返回 Flow 时,如果查询结果为空,Room 会发射一个空列表,而不是 null。所以你在 collect 的时候,一定要处理空列表的情况,别直接假定数据存在。
避坑指南:Flow 查询只对 @Query 注解的方法生效。如果你用 @RawQuery 或者直接操作 SQLiteDatabase,Room 不会自动触发重新查询。我曾经因为这个排查了半天,最后发现是用了原生查询。
collect数据流
在 ViewModel 或 Fragment 里 collect Flow 数据,通常有两种方式:
// 方式一:在 ViewModel 中使用
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val users: StateFlow<List<User>> = userDao.getAllUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
// 方式二:在 Fragment 中直接 collect
lifecycleScope.launch {
userDao.getAllUsers().collect { userList ->
// 更新 UI
adapter.submitList(userList)
}
}
我个人更推荐方式一,用 stateIn 把 Flow 转成 StateFlow。为什么?因为 StateFlow 会保留最新值,即使配置变更重建 Activity,UI 也能立即拿到数据,不用重新查询数据库。
这里有个细节:SharingStarted.WhileSubscribed(5000) 表示当最后一个订阅者消失后,等待 5 秒再停止上游 Flow。这样做的好处是,如果用户只是旋转屏幕,5 秒内重新订阅,不会重新触发数据库查询,节省性能。
小技巧:如果你在 Fragment 里直接 collect,记得用 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) 包裹,避免在后台时浪费资源。代码像这样:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.users.collect { ... }
}
}
与LiveData对比
很多同学会问:既然 LiveData 也能用,为什么还要学 Flow?我直接给你列个对比表:
| 特性 | LiveData | Flow |
|---|---|---|
| 数据源类型 | 热数据流(始终活跃) | 冷数据流(有订阅才活跃) |
| 线程切换 | 只能在主线程观察 | 可以在任意线程 collect |
| 操作符支持 | 有限(map、switchMap 等) | 丰富(map、filter、combine、flatMapLatest 等) |
| 生命周期感知 | 内置(自动取消订阅) | 需要配合 repeatOnLifecycle |
| 背压处理 | 不支持 | 支持(buffer、conflate 等) |
| 与协程集成 | 需要额外转换 | 原生支持 |
你看,Flow 在灵活性和功能上明显更强。但 LiveData 也有它的优势:简单、易用、生命周期感知是内置的。我个人习惯是:如果只是简单的 UI 数据绑定,用 LiveData 完全够用;但如果涉及复杂的操作符链、背压处理或者需要与协程深度集成,Flow 是更好的选择。
我记得有一次重构项目,把 LiveData 全部换成 Flow,结果发现代码量并没有减少太多,反而因为要处理生命周期问题多写了一些样板代码。所以我的建议是:不要盲目追求新技术,根据场景选择最合适的工具。
总结:Flow 和 LiveData 不是替代关系,而是互补关系。Room 官方推荐使用 Flow,因为它能更好地与协程配合,实现响应式数据库操作。但如果你项目里已经大量使用 LiveData,也没必要强行迁移。
知识体系结构图
这张图把今天的内容串起来了。你看,Flow 简介是基础,DAO 返回 Flow 是核心用法,collect 是消费端,最后和 LiveData 对比帮你做技术选型。我个人觉得,把这四个点吃透,你在 Room 里用 Flow 就不会有太大问题了。
最后说一句:Flow 的学习曲线确实比 LiveData 陡一点,但一旦你掌握了 stateIn、flatMapLatest 这些操作符,你会发现它能做的事情比 LiveData 多得多。我在项目里用 Flow 做搜索防抖、多表关联查询,代码写起来非常优雅。
公众号:蓝海资料掘金营,微信deep3321