14、Room与LiveData/Flow:让数据流动起来

说实话,Room 本身只是一个 ORM 框架,它帮我们把 SQLite 的增删改查包装得漂漂亮亮。但真正让 Room 变得「好用」的,是它和 LiveData、Flow 的深度整合。我刚开始用 Room 的时候,还傻傻地手动刷新 UI,后来才发现——原来数据变化可以自动通知 UI,根本不用我操心。

这一章,我们就来聊聊 Room 怎么和 LiveData、Flow 配合,实现真正的响应式数据查询。说白了,就是让数据库变成「活」的——数据一变,UI 自动跟着变。

14.1 LiveData 响应式查询

LiveData 是 Android 架构组件里的老面孔了。它的核心能力就一个:数据变化时,自动通知观察者。Room 对 LiveData 的支持非常自然——你只需要在 DAO 方法的返回类型上写 LiveData,Room 就会自动帮你做数据观察。

核心要点:Room + LiveData = 自动刷新。你只管查,剩下的交给 Room。

来看一个例子。假设我们有一个用户表,我想实时展示所有用户列表:

@Dao
public interface UserDao {
    @Query("SELECT * FROM users ORDER BY id DESC")
    LiveData<List<User>> getAllUsers();
}

就这么简单。返回类型从 List<User> 改成 LiveData<List<User>>,Room 就会在底层注册一个 ContentObserver,监听这张表的变化。一旦有数据插入、更新或删除,LiveData 就会自动发射新数据。

我在项目中遇到过一个问题:LiveData 在 Activity 重建时会重新触发。比如屏幕旋转,LiveData 会把最新数据再发一遍。这其实不是 bug,而是特性——它保证了 UI 总能拿到最新数据。但如果你不小心,可能会在 onCreate 里重复触发一些副作用操作。我的建议是:observe 而不是 observeForever,前者会自动处理生命周期。

小技巧:如果你只想在数据变化时做一次操作,可以用 Transformations.distinctUntilChanged() 过滤掉重复数据。

14.2 Flow 协程查询

LiveData 虽然好用,但它有个硬伤——不支持协程。LiveData 的 observe 是在主线程回调的,你不能在里面直接做耗时操作。而 Flow 就不一样了,它是 Kotlin 协程的亲儿子,天然支持异步、背压、操作符链。

我个人习惯是:新项目一律用 Flow。LiveData 只保留在需要和 Java 代码混用的场景。

Room 对 Flow 的支持同样简单:

@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY id DESC")
    fun getAllUsers(): Flow<List<User>>
}

你看,就是把 LiveData 换成 Flow。但背后的机制完全不同:Flow 是冷数据流,每次有新的收集者(collector)时,它才会重新执行查询。而且 Flow 天然支持协程取消——如果 ViewModel 被清除了,对应的协程也会自动取消,不会泄漏。

我曾经踩过一个坑:Flow 在 Room 中默认是「每次表变化都重新查询」。如果你的表更新非常频繁(比如每秒几十次),那 UI 可能会被频繁刷新。解决办法是用 distinctUntilChanged() 操作符:

@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY id DESC")
    fun getAllUsers(): Flow<List<User>>

    // 在 Repository 层做去重
    fun getUsersDistinct(): Flow<List<User>> {
        return getAllUsers().distinctUntilChanged()
    }
}

嗯,这里要注意:distinctUntilChanged() 比较的是数据内容,不是引用。如果你的数据对象没有正确实现 equals()hashCode(),它可能不会生效。

警告:Flow 的 collect 是挂起函数,必须在协程作用域内调用。不要在 Activity 或 Fragment 里直接 lifecycleScope.launch 去 collect,否则生命周期管理会很麻烦。建议用 repeatOnLifecycleflowWithLifecycle

14.3 Room 与 ViewModel 结合

ViewModel 是数据的「管家」。它负责从 Repository 拿数据,然后暴露给 UI。Room + LiveData/Flow + ViewModel 的组合,是官方推荐的架构模式。

我一般这样组织代码:

class UserViewModel(
    private val userRepository: UserRepository
) : ViewModel() {

    // 方式一:用 LiveData
    val usersLiveData: LiveData<List<User>> = userRepository.getAllUsers()

    // 方式二:用 Flow(推荐)
    val usersFlow: StateFlow<List<User>> = userRepository.getAllUsers()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

这里有个细节:stateIn 把 Flow 转成了 StateFlow。StateFlow 和 LiveData 很像——它总是持有最新值,新订阅者能立刻拿到当前数据。但 StateFlow 是 Kotlin 原生的,支持协程,而且没有 LiveData 的那些历史包袱。

我在项目中遇到过一个问题:StateFlow 的初始值怎么给? 如果你给了一个空列表,UI 会先闪一下空状态,然后才显示真实数据。我的做法是:SharingStarted.WhileSubscribed(5000),这样当 Activity 进入后台时,Flow 会延迟 5 秒再取消订阅,避免频繁重建。

最佳实践:ViewModel 里永远不要暴露 Mutable 类型。对外只暴露 LiveData 或 StateFlow,数据修改通过函数方法进行。

14.4 数据观察与 UI 自动更新

数据观察的最终目的,就是让 UI 自动更新。说白了,你不需要手动调用 adapter.notifyDataSetChanged(),也不需要写什么 refreshUI() 方法。数据一变,UI 自己就变了。

在 Activity 或 Fragment 里,代码非常简洁:

// 用 LiveData
viewModel.usersLiveData.observe(this) { users ->
    adapter.submitList(users)
}

// 用 Flow(配合 lifecycle-runtime-ktx)
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.usersFlow.collect { users ->
            adapter.submitList(users)
        }
    }
}

你看,核心就两行代码。但这里有个容易忽略的点:数据观察的时机。LiveData 的 observe 只在 STARTED 状态时才会收到数据,而 Flow 的 repeatOnLifecycle 也是同理。这意味着——当 Activity 在后台时,数据变化不会触发 UI 更新,避免了无谓的刷新。

我曾经犯过一个错误:在 Fragment 的 onViewCreated 里直接 collect Flow,没有用 repeatOnLifecycle。结果 Fragment 进入后台再回来时,collect 已经取消了,UI 不再更新。嗯,从那以后我就记住了——生命周期感知,不是一句空话

14.5 生命周期感知

生命周期感知,是 LiveData 和 Flow 最大的区别之一。LiveData 是「天生」感知生命周期的——它知道观察者当前是活跃还是非活跃状态。而 Flow 本身没有生命周期概念,需要借助 repeatOnLifecycleflowWithLifecycle 来实现。

我画了一张图,帮你理清 Room、LiveData、Flow、ViewModel 之间的关系:

Room + LiveData/Flow + ViewModel 架构图 SQLite 数据库 物理存储层 Room DAO + Entity + Database Repository 数据仓库层 ViewModel LiveData / StateFlow Activity / Fragment observe / collect 生命周期感知 repeatOnLifecycle 数据流方向:数据库 → Room → Repository → ViewModel → UI

从这张图你能看到:数据从数据库一路流到 UI,每一层各司其职。Room 负责监听表变化,Repository 做数据转换,ViewModel 暴露不可变数据,UI 层做生命周期感知的订阅。

最后,我总结一下 LiveData 和 Flow 的选择建议:

场景 推荐方案 原因
Java 项目 LiveData Java 不支持协程,LiveData 是唯一选择
Kotlin 新项目 Flow + StateFlow 协程原生支持,操作符丰富,性能更好
需要背压处理 Flow LiveData 没有背压概念
简单数据观察 LiveData API 更简单,学习成本低
复杂数据转换 Flow map、flatMapLatest 等操作符非常强大

我个人现在的习惯是:新项目全部用 Flow。LiveData 虽然简单,但 Flow 的灵活性和协程生态实在太香了。你想想看,一个 flatMapLatest 就能搞定「根据用户输入实时搜索」这种需求,LiveData 得写多少 Transformations 啊?

避坑指南:我曾经在 ViewModel 里直接暴露了 MutableStateFlow,结果 UI 层直接修改了数据源,导致数据不一致。记住:对外永远只暴露不可变的 StateFlow 或 LiveData

好了,这一章的内容就到这里。Room 和 LiveData/Flow 的组合,说白了就是让数据「活」起来。你只需要关注业务逻辑,数据流动和 UI 更新,Room 和架构组件帮你搞定。


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