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 层的完整流程,以及各个关键节点的作用:
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