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 会自动帮你做两件事:

  1. 首次查询时发射一次数据
  2. 当 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 + Room 知识体系 Room + Flow Flow 简介 冷数据流 生产者/消费者模型 中间操作符 DAO 返回 Flow @Query 自动重查 增删改触发更新 空列表 vs null collect 数据流 stateIn 转 StateFlow repeatOnLifecycle WhileSubscribed 与 LiveData 对比 热 vs 冷 · 线程灵活性 · 操作符丰富度 · 生命周期处理

这张图把今天的内容串起来了。你看,Flow 简介是基础,DAO 返回 Flow 是核心用法,collect 是消费端,最后和 LiveData 对比帮你做技术选型。我个人觉得,把这四个点吃透,你在 Room 里用 Flow 就不会有太大问题了。

最后说一句:Flow 的学习曲线确实比 LiveData 陡一点,但一旦你掌握了 stateIn、flatMapLatest 这些操作符,你会发现它能做的事情比 LiveData 多得多。我在项目里用 Flow 做搜索防抖、多表关联查询,代码写起来非常优雅。


公众号:蓝海资料掘金营,微信deep3321