8、Flow在UI层的应用:collect与collectLatest、Lifecycle.repeatOnLifecycle、StateFlow的观察

说实话,Flow 在 ViewModel 里跑得再欢,最终还是要落到 UI 层来展示。很多新手把 Flow 从网络层一路撸到 ViewModel,结果在 Activity 或 Fragment 里直接用 lifecycleScope.launch 去 collect,然后就出事了——页面退到后台还在收数据,甚至导致崩溃。嗯,这节我们就来彻底解决这个问题。

8.1 collect 与 collectLatest:到底该用谁?

先看最基础的 collect。它的行为很简单:一个个处理,处理完一个再处理下一个。就像流水线上的工人,来一个零件装一个,装完再拿下一个。

// 基础用法
viewModel.someFlow.collect { value ->
    // 更新UI
    textView.text = value
}

但这里有个坑。如果上游发射得很快,而 collect 里的操作很慢(比如做了复杂计算或网络请求),就会造成背压。数据会堆积在缓冲区,等你处理完一个,下一个可能已经是过时的了。

我在项目中遇到过这样一个场景:搜索框输入关键词,每输入一个字符就发射一次 Flow,collect 里去做网络请求。结果用户快速输入“Kotlin”时,collect 老老实实发了 K、Ko、Kot、Kotl……五次请求。最后界面显示的是“Kotl”的结果,因为最后一个请求还没回来。

这时候就该 collectLatest 出场了。它的行为是:如果上一个还没处理完,新值来了,直接取消上一个,处理最新的

viewModel.searchFlow.collectLatest { query ->
    // 如果上一个请求还没完,会被自动取消
    val result = repository.search(query)
    textView.text = result
}

核心区别一句话总结:

  • collect:按顺序处理,不丢数据,但可能处理过时数据
  • collectLatest:只关心最新的,会取消旧任务,适合搜索、刷新等场景

我个人习惯:UI 更新类的操作(比如显示列表、更新文本)用 collect 就够了,因为 UI 更新本身很快。但涉及网络请求或数据库查询的,强烈建议用 collectLatest,避免无谓的浪费。

8.2 Lifecycle.repeatOnLifecycle:安全收集的黄金标准

直接上代码,看看最常见的错误写法:

// ❌ 错误示范
lifecycleScope.launch {
    viewModel.someFlow.collect { value ->
        textView.text = value
    }
}

为什么错?因为 lifecycleScope 虽然会在 LifecycleOwner 销毁时取消协程,但它不会在页面进入 STOPPED 状态时暂停收集。如果你的 Flow 是热流(比如 StateFlow),页面退到后台后,collect 还在运行,数据还在更新 UI——但 UI 已经不可见了。轻则浪费资源,重则引发 IllegalStateException。

正确的做法是用 repeatOnLifecycle

// ✅ 推荐写法
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.someFlow.collect { value ->
            textView.text = value
        }
    }
}

它的行为很优雅:

  • 当 Lifecycle 至少是 STARTED 状态时,开始收集
  • 当 Lifecycle 进入 STOPPED 状态时,自动取消收集协程
  • 当 Lifecycle 重新回到 STARTED 时,重新开始收集

我曾经在一个直播类 App 里踩过这个坑。用户切到后台再回来,发现界面卡住了——因为 collect 在后台一直运行,但某些 UI 操作被系统拦截了,导致状态不一致。换成 repeatOnLifecycle 后,问题彻底解决。

注意:repeatOnLifecycle 会重新订阅 Flow。如果你的 Flow 是冷流(比如用 flow builder 创建的),每次重新订阅都会重新执行上游逻辑。对于 StateFlow 这种热流,重新订阅会拿到最新的值,通常没问题。

8.3 StateFlow 的观察:UI 层的最佳拍档

StateFlow 在 UI 层的使用,说白了就是观察状态变化。它和 LiveData 很像,但更强大——因为它完全基于 Kotlin 协程,没有 Android 依赖。

在 ViewModel 里定义:

class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _uiState.value = UiState(loading = true)
            val data = repository.fetchData()
            _uiState.value = UiState(data = data)
        }
    }
}

在 Fragment 里观察:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            // 根据 state 更新 UI
            progressBar.isVisible = state.loading
            textView.text = state.data
        }
    }
}

这里有个细节:StateFlow 默认会去重。如果两次发射的值相同(通过 equals 判断),collect 不会收到重复通知。这比 LiveData 更智能,能避免不必要的 UI 刷新。

避坑指南:我曾经在项目里用 MutableStateFlow 时,直接暴露了 _uiState 给外部,结果别的模块直接修改了状态,导致 UI 和状态不同步。记住:永远用 asStateFlow() 暴露不可变的 StateFlow,修改只能通过 ViewModel 内部。

8.4 知识体系图:Flow 在 UI 层的完整链路

下面这张图展示了 Flow 从 ViewModel 到 UI 层的完整流程,以及各个关键节点的作用:

Flow 在 UI 层的完整链路 ViewModel StateFlow / SharedFlow 发射 repeatOnLifecycle Lifecycle.State.STARTED 收集 collect / collectLatest 选择策略 关键说明: 1. ViewModel 通过 StateFlow 暴露不可变状态 2. repeatOnLifecycle 确保只在可见时收集 3. collect 按顺序处理,collectLatest 只处理最新值 4. 页面进入后台 → 自动取消收集 → 回到前台 → 重新订阅 常见错误: 直接使用 lifecycleScope.launch + collect → 后台仍运行

8.5 实战建议:组合使用才是王道

在实际项目中,我通常这样组合使用:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        // 对于 UI 状态,用 collect 保证每个状态都更新
        viewModel.uiState.collect { state ->
            renderUi(state)
        }
    }
}

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        // 对于搜索等高频操作,用 collectLatest
        viewModel.searchResults.collectLatest { results ->
            updateSearchList(results)
        }
    }
}

你想想看,这样写的好处是什么?每个 Flow 都有自己的生命周期管理,互不干扰。而且 repeatOnLifecycle 保证了资源不会浪费——页面不可见时,所有收集都暂停了。

一个小技巧:如果你在 Fragment 里需要同时收集多个 Flow,可以开多个协程,每个都用 repeatOnLifecycle 包裹。不要试图在一个 collect 里处理所有事情,那样代码会变得难以维护。

嗯,Flow 在 UI 层的应用,核心就是这三板斧:collect/collectLatest 选对策略,repeatOnLifecycle 保证安全,StateFlow 提供状态。把这三点吃透,你的响应式 UI 就不会再出那些奇奇怪怪的问题了。


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