Flow中的conflate()详解

69 阅读3分钟

它到底干啥?

  • 核心:当下游很慢时,**跳过中间值,只保留“最新值”**再交给下游处理。
  • 本质实现:等价于在这里放了一个 Channel.CONFLATED 的缓冲边界(近似 buffer(capacity = Channel.CONFLATED))。
  • 效果:上游不会被下游速度拖慢;但会丢中间值(非等价过滤,是直接替换)。
  upstream (快)  --[ 只存1个最新值 | 覆盖旧值 ]-->  downstream (慢)

何时用 / 何时不用

✅ 适合

  • 状态类数据:只关心“此刻状态”,不在乎中间过渡(如进度、位置、UI 状态)。

  • 高频轻量事件:想解耦上下游,让上游跑满速,下游慢慢“追最新”。

❌ 不适合

  • 每个事件都必须处理的场景(支付点击、打点、可靠消息)。这些用 buffer(SUSPEND) 或持久队列。

与其它操作符怎么区分?

  • buffer(1, DROP_OLDEST) :语义非常接近(保最新、丢最老)。conflate()可视作其语义化简写;CONFLATED 在实现上更“替换式”,一般更省事。

  • collectLatest { ... } / mapLatest { ... }会取消前一个“下游处理”。适合重计算/可中断的耗时任务(如搜索联想渲染)。

    conflate()不取消下游当前处理,只是跳过中间输入

  • distinctUntilChanged() :按相等性去重;conflate()不比较相等性,只是因背压跳过中间值。二者可搭配:conflate().distinctUntilChanged()。

  • debounce()/sample()按时间取样;conflate()是按背压合并,无时间窗口概念。

放置位置(非常关键)

  • 把 conflate() 放在慢步骤之前,让慢步骤只处理“最新值”:
source
  .conflate()      // ← 切断背压、合并中间值
  .map { slow(it) }
  .collect { render(it) }
  • 若慢步骤在 conflate() 之前
source
  .map { slow(it) } // ← 这里已经慢了,conflate 放后面救不了它
  .conflate()
  .collect { render(it) }

此时上游慢步骤仍会对每个值运行,达不到“省工”的目的。

若你希望来新值就中断慢计算,应改用 mapLatest/flatMapLatest/collectLatest

与flowOn的关系

  • flowOn 会引入一个缓冲边界;conflate()再引入一个**“只留最新”**的边界。
  • 组合示例(IO 侧狂喷、UI 侧只拿最新):
source
  .map { ioHeavy(it) }
  .flowOn(Dispatchers.IO) // 上下文切换处已有缓冲
  .conflate()              // 再次压扁,只给下游“最新”
  .collect { uiRender(it) }

典型模式

  1. 只关心最新状态(UI 渲染)
viewModel.state   // 频繁 setState
  .conflate()
  .collect { render(it) } // 渲染总是“最新屏幕状态”
  1. 进度/位置流
download.progressFlow() // 0..100 高频
  .conflate()
  .collect { updateProgressBar(it) }
  1. 配合去重防抖
textChanges()                 // 高频输入
  .debounce(250)              // 时间降噪
  .distinctUntilChanged()     // 相同文本不触发
  .flatMapLatest { query ->   // 新查询取消旧查询
      repo.search(query)
  }
  .conflate()                 // 后续 UI 只取最新结果帧
  .collect { showResult(it) }

与 StateFlow / SharedFlow

  • StateFlow天生是“最新值”容器,更新本就具备合并语义;对它再 conflate()通常收益不大/冗余

  • SharedFlow可通过 replay=0/1 + extraBufferCapacity + onBufferOverflow=DROP_OLDEST 配成“只留最新”的效果;此时再 conflate()也多半冗余,除非你刻意再加一层边界

它会“乱序”吗?

  • conflate()不会打乱保留下来的元素的相对顺序,但会跳过中间若干个,所以表现像“突然跳到最新”。

小对比示例

1. conflate:不取消下游,只跳过中间输入

(1..100).asFlow()
  .onEach { delay(10) }             // 上游很快
  .conflate()
  .collect { value ->
    delay(100)                      // 下游很慢
    println("got $value")           // 打印数目 << 100,且是“跳号”的
}

2. collectLatest:直接取消上一个处理

(1..100).asFlow()
  .onEach { delay(10) }
  .collectLatest { value ->
    delay(100)                      // 来新值就取消旧的 delay
    println("only latest finishes: $value")
}

易坑与建议

  • 易坑:业务需要“每条必达/必处理”时误用 conflate() → 丢事件

  • 建议容量:conflate()无需你设置容量;若你需要“保最新 + 再多留几条”,用

    buffer(n, onBufferOverflow = DROP_OLDEST)(如 n=8/16)更灵活。

  • 重计算省不下来? 把 conflate()放到重计算之前,或改用 mapLatest 实现“来新就中断旧”。

一句话总结

conflate() = “只给下游最新值”的背压解法:不取消下游处理、跳过中间输入;适合状态/进度/UI,不适合必须逐条处理的事件流。