Flow的collect 和 collectLatest

21 阅读3分钟

一句话对比

  • 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)
        }
    }
}

速查表

collectcollectLatest
是否顺序处理否(新值会取消旧处理)
是否会丢中间值不会(旧处理被取消)
背压行为上游被背压(除非 buffer/conflate)上游仍可能被背压,需配置缓冲
适用场景必须每条处理只关心最新、可取消旧处理
与 conflate()collect + conflate:不取消当前处理collectLatest:会取消当前处理

总结

  • 要“每条必达” → collect。
  • 要“只要最新” → collectLatest。
  • 想减少阻塞 → 在上游加 buffer() 或配置 SharedFlow 的缓冲/溢出策略。
  • 别忘了 collectLatest 只取消 你的处理块,你自己开的子协程需要可取消管理。