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,否则生命周期管理会很麻烦。建议用 repeatOnLifecycle 或 flowWithLifecycle。
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 本身没有生命周期概念,需要借助 repeatOnLifecycle 或 flowWithLifecycle 来实现。
我画了一张图,帮你理清 Room、LiveData、Flow、ViewModel 之间的关系:
从这张图你能看到:数据从数据库一路流到 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