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。

⚠️ 常见错误示范:直接在 Fragment 的 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:生命周期安全收集流程

lifecycleScope + repeatOnLifecycle / flowWithLifecycle 安全收集流程 ViewModel 安全收集 Flow 生命周期管理流程 Room DAO Flow<List<User>> Repository flowOn(Dispatchers.IO) ViewModel 暴露 Flow,不切换 lifecycleScope 绑定 LifecycleOwner repeatOnLifecycle State.STARTED 自动启停 flowWithLifecycle 声明式单 Flow 包装 UI 更新 collect 主线程安全 ⭐ 最佳实践要点 • 使用 repeatOnLifecycle 包裹 collect • 或使用 flowWithLifecycle 声明式 • 始终在 viewLifecycleOwner 上操作

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,收集应自动重启")
    }
}
✅ 预期日志行为:当 Fragment 进入后台,日志停止输出 "收到 X 条用户数据";当 Fragment 重新回到前台,日志恢复输出,且不会出现数据积压导致的跳帧。

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 发射机制,开发者可以构建出 响应式、稳定且高性能 的数据展示层。