一句话对比
- collect { … } :顺序处理 每一个 发射值。你的 block 没执行完,上游会被背压(除非中途 buffer() 等)。
- collectLatest { … } :每来新值就 取消 之前那个 block,只处理“最新值”。适合“只渲染最新”的场景。
时间线示意(▲表示新值到达)
collect
emit v1 ▲ ──[处理v1 300ms]── emit v2 ▲ ──[处理v2]── emit v3 ▲ ──[处理v3]──
↑期间上游会被背压,直到下游处理完
collectLatest
emit v1 ▲ ──[处理v1……(未完成就被取消)]─╳
emit v2 ▲ ──[处理v2……(未完成又被取消)]─╳
emit v3 ▲ ──[处理v3 到底]
- 新值一到,上一次的 block 立即取消,立刻转去处理最新值。
- 注意:collectLatest 取消的是 你的处理块,并不等于“上游一定不被背压”。上游是否被阻塞,还取决于 缓冲策略(见下文“坑 1”)。
典型使用场景
-
✅ collectLatest
- 搜索框:用户快速输入,“只要最新结果”,旧请求/渲染取消。
- 图片/视频加载:快速切换列表项,旧图渲染取消。
- Compose UI 渲染:重绘较重,只保留最新状态。
-
✅ collect
- 日志、埋点:不能丢事件。
- 文件/数据库迁移:必须顺序处理每个任务。
- 严格的流水线:每个值都要到达终点。
与conflate()的区别
- collectLatest:取消旧值的处理块;下游会立刻转去处理最新值。
- conflate() :丢掉中间值,只保留最新,但不会取消当前处理;当前处理完成后再处理“最新那个”。
flow.conflate().collect { heavy(x) } // heavy(x) 不会被中途取消
flow.collectLatest { heavy(x) } // heavy(x) 可能被取消
常见坑 & 对策
坑 1:以为 collectLatest 就不会让上游挂起
-
如果你的上游是 MutableSharedFlow(replay=0, extraBufferCapacity=0, SUSPEND),emit() 仍可能挂起。
-
✅ 对策:
-
给 SharedFlow 配 extraBufferCapacity 或 onBufferOverflow = DROP_OLDEST/DROP_LATEST;
-
或在收集前加 .buffer()。
-
坑 2:collectLatest 只取消“你的块”,你自己 launch 的子协程不会自动停
-
✅ 对策:把子任务绑在一个可取消的 Job 上,来新值先 job?.cancel() 再启动新 launch。
坑 3:需要保证“每条都处理一次”却用了 collectLatest
- ✅ 对策:改回 collect,必要时加 buffer() 控制背压。
代码模板
1) 搜索框(防抖 + 只取最新)
val query = MutableStateFlow("")
val resultFlow = query
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { q ->
flow { emit(api.search(q)) } // 每次订阅触发一次请求
.flowOn(Dispatchers.IO)
}
lifecycleScope.launch {
resultFlow.collectLatest { list ->
render(list) // 上一轮渲染没完会被取消
}
}
2) 列表滚动加载图片(只显示最新可见项)
lifecycleScope.launch {
visibleImageRequests() // Flow<ImageRequest>
.buffer(Channel.UNLIMITED) // 允许排队,不阻塞上游
.collectLatest { req ->
// 旧图渲染取消
val bmp = withContext(Dispatchers.IO) { loader.load(req) }
imageView.setImageBitmap(bmp)
}
}
3) 事件不能丢(用collect)
val eventFlow = MutableSharedFlow<Event>(replay = 0, extraBufferCapacity = 64)
lifecycleScope.launch {
eventFlow.collect { e -> // 逐个处理,不丢
handleEvent(e)
}
}
4) Compose / Lifecycle 推荐收集方式
// Fragment
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collectLatest { state ->
render(state)
}
}
}
// Compose
val state by viewModel.uiState.collectAsStateWithLifecycle()
5)collectLatest + 手动取消子任务
lifecycleScope.launch {
var job: Job? = null
flow.collectLatest { value ->
job?.cancel()
job = launch {
// 子任务随 collectLatest 的取消而取消
heavyWork(value)
}
}
}
速查表
| collect | collectLatest | |
|---|---|---|
| 是否顺序处理 | 是 | 否(新值会取消旧处理) |
| 是否会丢中间值 | 不会 | 会(旧处理被取消) |
| 背压行为 | 上游被背压(除非 buffer/conflate) | 上游仍可能被背压,需配置缓冲 |
| 适用场景 | 必须每条处理 | 只关心最新、可取消旧处理 |
| 与 conflate() | collect + conflate:不取消当前处理 | collectLatest:会取消当前处理 |
总结
- 要“每条必达” → collect。
- 要“只要最新” → collectLatest。
- 想减少阻塞 → 在上游加 buffer() 或配置 SharedFlow 的缓冲/溢出策略。
- 别忘了 collectLatest 只取消 你的处理块,你自己开的子协程需要可取消管理。