第一篇:为什么多个 Flow collect 必须 launch?——一篇讲透 Android 协程生命周期 引言很多 Android 开发在刚接触 Kotlin 协程 Flow 时都会写出类似代码lifecycleScope.launch { mViewModel.loadingFlow.collect { // 更新 loading } mViewModel.errorFlow.collect { // 处理错误 } }然后发现第二个 collect 根本不执行。很多人第一反应是不是协程有问题 是不是 Flow 有 bug实际上这恰恰说明你还没有真正理解collect 本质Flow 生命周期协程结构化并发lifecycleScoperepeatOnLifecycle今天这篇文章 我们就从工程视角真正讲透Android 中 Flow 为什么不能乱 collect。一、collect 本质到底是什么很多人误以为collect {}类似list.forEach {}执行完就结束。实际上collect 通常是长期挂起任务。例如mViewModel.loadingFlow.collect { isLoading - }本质是一直监听 loadingFlow只要数据变化就回调 collect也就是说collect 默认不会自动结束。它会一直挂起等待新数据。二、为什么第二个 collect 不执行来看错误示例lifecycleScope.launch { mViewModel.loadingFlow.collect { Log.d(TAG, loading) } mViewModel.errorFlow.collect { Log.d(TAG, error) } }问题就在这里loadingFlow.collect {}会一直挂起。所以后面的代码根本执行不到。即第一个 collect 永远不结束 ↓ 第二个 collect 永远没机会执行三、正确写法多个 collect 必须 launch正确写法lifecycleScope.launch { launch { mViewModel.loadingFlow.collect { Log.d(TAG, loading) } } launch { mViewModel.errorFlow.collect { Log.d(TAG, error) } } }为什么这样可以因为launch {}会创建子协程。结构变成父协程 ├── 子协程Acollect loading └── 子协程Bcollect error这样两个 collect 并发执行 互不阻塞四、lifecycleScope 到底是什么很多人觉得lifecycleScope.launch {}只是开个协程其实不是。lifecycleScope 本身就是 CoroutineScope。它内部已经绑定LifecycleJobMain Dispatcher所以lifecycleScope.launch {}本质是在 Activity 生命周期范围内 创建一个子协程五、lifecycleScope 到底解决了什么核心Activity 销毁时自动 cancel 协程。即Activity finish ↓ lifecycleScope cancel ↓ 所有子协程 cancel因此lifecycleScope.launch { }不用担心页面销毁后协程泄漏六、但 lifecycleScope 不解决一个问题很多人以为lifecycleScope.launch {}已经彻底解决生命周期问题。实际上不是。因为页面进入后台时 Activity 不一定销毁例如按 Home 键打开别的页面锁屏此时lifecycleScope 还活着 collect 还在继续这就会导致后台继续收集数据后台继续更新 UI后台继续弹 Toast后台继续执行逻辑显然不合理。七、repeatOnLifecycle 真正解决了什么正确写法lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { mViewModel.loadingFlow.collect { } } } }它的核心页面可见时开始 collect。页面不可见时停止 collect。即进入前台 ↓ 开始收集 进入后台 ↓ 停止收集 再次回到前台 ↓ 重新收集八、企业级推荐写法很多人会这样写lifecycleScope.launch { repeatOnLifecycle { collect A } } lifecycleScope.launch { repeatOnLifecycle { collect B } }其实不是错。完全能跑。但问题是生命周期入口过多 结构分散 维护困难九、真正推荐的结构化写法推荐private fun observeUiState() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { mViewModel.loadingFlow.collect { isLoading - } } launch { mViewModel.errorFlow.collect { error - } } launch { mViewModel.loginFlow.collect { user - } } } } }结构变成lifecycleScope └── ObserverRoot ├── loadingFlow ├── errorFlow └── loginFlow优势生命周期入口统一所有 UI 状态统一管理更符合结构化并发思想更适合大型项目维护十、协程真正核心结构化并发很多人觉得launch {}只是开线程实际上Kotlin 协程核心是Structured Concurrency结构化并发即所有协程都应该有父协程。因此launch {}本质是在当前 CoroutineScope 下 创建一个子协程节点所以lifecycleScope.launch { launch {} launch {} }本质结构lifecycleScope └── Parent ├── ChildA └── ChildB十一、Job 是什么val job launch { }返回Job本质协程控制器。可以job.cancel()取消协程。十二、为什么 collect 可以 cancel因为Flow collect 是可取消挂起函数。当job.cancel()时collect 收到取消信号 ↓ 停止监听 ↓ 协程结束十三、最后总结这一轮真正要理解的核心协程不是“开线程”。而是在协程树中 创建一个受生命周期与父 Job 管理的任务节点。而Flow collect本质是长期挂起监听任务。所以多个 collect 必须并发 launch。最终企业级推荐写法lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { mViewModel.loadingFlow.collect { } } launch { mViewModel.errorFlow.collect { } } launch { mViewModel.loginFlow.collect { } } } }下篇预告下一篇我们继续《StateFlow 与 SharedFlow 到底怎么选——一篇讲透 Android 热流设计》深入讲透StateFlow 为什么必须有初始值SharedFlow 为什么适合事件Toast 为什么不能用 StateFlow页面旋转为什么会重复消费replay 到底是什么emit 与 tryEmit 的真正区别热流与冷流本质区别企业级 UI 状态设计方案