ViewModel 中安全收集 Flow:lifecycleScope 与 repeatOnLifecycle、flowWithLifecycle
在 Android 架构组件中,ViewModel 通常负责从 Repository 或 Room DAO 获取数据,并将数据以 Flow 的形式暴露给 UI 层。如果直接在 Activity 或 Fragment 的 onCreate 或 onViewCreated 中启动协程收集 Flow,而不考虑 Lifecycle 绑定,极易在后台状态或配置变更时引发 ANR 或内存泄漏。本章围绕三个核心 API——lifecycleScope、repeatOnLifecycle、flowWithLifecycle——讲解如何在 ViewModel 与 UI 之间建立 生命周期安全 的 Flow 收集通道。
4.1 问题根源:未绑定的协程与后台持续发射
Room DAO 返回的 Flow 在数据库表发生任何变更时都会重新发射数据。如果 Activity 进入后台(onStop)后协程仍然活跃,且 UI 层尝试更新视图,轻则抛出 IllegalStateException,重则因主线程积压过多任务导致 ANR。
onViewCreated 中调用 lifecycleScope.launch 收集 Flow,未使用 repeatOnLifecycle 包裹。当 Fragment 进入后台,协程仍在运行,数据库的每一次变更都会尝试更新已销毁的 View,造成资源浪费和潜在的崩溃。
4.2 lifecycleScope:绑定了 Lifecycle 的协程作用域
lifecycleScope 是 LifecycleOwner(Activity / Fragment)的扩展属性,它提供了与 Lifecycle 绑定的 CoroutineScope。当 Lifecycle 到达 DESTROYED 状态时,作用域自动取消,从而避免泄漏。但仅使用 lifecycleScope.launch 仍不够,因为它只在 DESTROYED 时取消,而在 STOPPED 或 STARTED 状态切换时不会自动重启或暂停收集。
4.3 repeatOnLifecycle:状态驱动的自动启停
repeatOnLifecycle(Lifecycle.State state) 是一个挂起函数,它会等待 Lifecycle 达到指定的 state 后启动内部的协程块,并在 Lifecycle 低于该状态时自动取消协程块。当 Lifecycle 再次回到目标状态时,重新启动协程块。这实现了 自动启停 机制,保证 Flow 只在 UI 可见(如 STARTED)时被收集。
| Lifecycle 状态 | 协程行为 | UI 更新安全性 |
|---|---|---|
| CREATED | 未开始收集 | 安全,不访问 View |
| STARTED | 开始收集,UI 可见 | 安全,View 存在 |
| RESUMED | 继续收集,可交互 | 安全,View 活跃 |
| DESTROYED | 取消协程,清理资源 | 安全,View 已销毁 |
4.4 flowWithLifecycle:声明式单 Flow 安全收集
flowWithLifecycle() 是 Kotlin Flow 的扩展函数,它将一个 Flow 包装为仅在 Lifecycle 处于特定状态以上时才发射数据的 Flow。当 Lifecycle 低于目标状态时,上游 Flow 被取消;当状态恢复时,重新订阅上游。这是 repeatOnLifecycle 在单 Flow 场景下的 声明式替代,代码更加简洁。
4.5 完整实现流程:Room DAO → ViewModel → UI
下面通过一个完整的用户列表示例,展示从 Room DAO 定义到 UI 安全收集的完整链路。
步骤 1:Room DAO 定义
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name ASC")
fun observeAllUsers(): Flow<List<User>>
}
步骤 2:Repository 层
class UserRepository(private val userDao: UserDao) {
fun getUsers(): Flow<List<User>> {
return userDao.observeAllUsers()
.flowOn(Dispatchers.IO) // 数据库查询切换到 IO 线程
.conflate() // 丢弃中间值,防止积压
}
}
步骤 3:ViewModel 暴露 Flow
class UserViewModel(private val repository: UserRepository) : ViewModel() {
// 直接暴露 Repository 的 Flow,不在此处切换调度器
val users: Flow<List<User>> = repository.getUsers()
}
步骤 4:UI 层安全收集(使用 repeatOnLifecycle)
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
// 只在 STARTED 状态以上收集,自动启停
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.users.collect { users ->
// 更新 UI,此处已在主线程
updateUserList(users)
}
}
}
}
private fun updateUserList(users: List<User>) {
// 安全更新 RecyclerView
}
}
步骤 5:UI 层安全收集(使用 flowWithLifecycle 声明式)
class UserFragmentAlt : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.users
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { users ->
updateUserList(users)
}
}
}
}
4.6 调度器配置与线程切换策略
Room DAO 的 Flow 默认在 底层 Binder 线程 上发射,但为了确保数据库读取不阻塞主线程,应在 Repository 层使用 flowOn(Dispatchers.IO) 将上游切换到 IO 线程。收集端(Fragment)无需切换调度器,因为 collect 运行的协程已在 lifecycleScope 的默认调度器(Dispatchers.Main.immediate)上,UI 更新直接在主线程执行。
| 层级 | 调度器 | 作用 |
|---|---|---|
| Room DAO | 自动 Binder 线程 | 监听数据库表变更 |
| Repository | Dispatchers.IO(通过 flowOn) |
执行查询,避免主线程阻塞 |
| ViewModel | 无(透传 Flow) | 保持数据层与 UI 层分离 |
| Fragment / Activity | Dispatchers.Main.immediate |
安全更新 UI |
4.7 内联 SVG:生命周期安全收集流程
4.8 调试与验证:确认协程正确启停
使用 Android Studio 的 Coroutine Debugger 或日志打印来验证 Flow 收集是否按生命周期启停。
class UserFragmentDebug : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
Log.d("LifecycleFlow", "开始收集 Flow")
viewModel.users.collect { users ->
Log.d("LifecycleFlow", "收到 ${users.size} 条用户数据")
updateUserList(users)
}
}
}
}
override fun onStop() {
super.onStop()
Log.d("LifecycleFlow", "Fragment 进入 STOPPED,收集应自动取消")
}
override fun onStart() {
super.onStart()
Log.d("LifecycleFlow", "Fragment 进入 STARTED,收集应自动重启")
}
}
4.9 常见错误与规避
- 错误 1:在 ViewModel 中使用 lifecycleScope
ViewModel 不继承LifecycleOwner,无法直接使用lifecycleScope。应在 Fragment / Activity 中使用viewLifecycleOwner.lifecycleScope。 - 错误 2:使用 lifecycleScope.launch 直接收集,未包裹 repeatOnLifecycle
导致后台仍继续收集,可能更新已销毁的 View 或引发 ANR。 - 错误 3:在 repeatOnLifecycle 内部切换调度器
collect默认在主线程执行 UI 更新,无需额外切换。如需耗时操作,应在上游通过flowOn处理。 - 错误 4:混淆 viewLifecycleOwner 与 this(Fragment 本身)
在 Fragment 中必须使用viewLifecycleOwner,而非this,以确保在视图销毁时取消协程。
4.10 性能与稳定性优化建议
| 优化点 | 方案 | 收益 |
|---|---|---|
| 避免中间值积压 | 在上游使用 conflate() 操作符 |
丢弃中间状态,减少 UI 更新次数 |
| 减少不必要收集 | 使用 distinctUntilChanged() 去重 |
避免相同数据重复触发 UI 刷新 |
| 控制背压 | 结合 buffer() 或 conflate() |
防止发射速度远快于消费速度 |
| 生命周期精确绑定 | 使用 Lifecycle.State.STARTED 而非 RESUMED |
在可见时即可收集,不等待可交互 |
| 测试覆盖 | 编写 LifecycleObserver 测试协程启停 |
提前发现生命周期不匹配问题 |
4.11 总结
lifecycleScope + repeatOnLifecycle 或 flowWithLifecycle 是 Android 官方推荐的 安全收集 Flow 的标准方案。它们将协程的启停与 UI 生命周期强绑定,从根本上杜绝了后台更新 View 导致的 ANR 与内存泄漏。配合 Room DAO 的 Flow 发射机制,开发者可以构建出 响应式、稳定且高性能 的数据展示层。