Flow操作符进阶:flatMapConcat、flatMapMerge、flatMapLatest
这三个操作符,说实话,是处理嵌套 Flow 的核心武器。我个人刚接触时也被绕晕过——明明都是把内层 Flow 打平,为什么跑起来效果天差地别?
别急,我们先画一张图,把它们的逻辑铺开,然后再手敲代码验证。
1. flatMapConcat——老老实实排队
这个操作符的做法很简单:等前一个内层 Flow 全部发射完,再启动下一个。你想想看,就像银行柜台只有一个窗口,前面的人没办完,后面的人就得一直等着。
核心行为:串行执行,严格有序。内层 Flow 的发射顺序与上游元素顺序完全一致,且不会出现并发。
我在项目中用过一次:从数据库分批读取用户列表,每批返回一个 Flow,然后把这些 Flow 依次 merge 成一个。当时用了 flatMapConcat,结果发现用户体验很糟糕——第二批的用户必须等第一批全部加载完才能显示。嗯,这就是串行的代价。
代码示例:
flowOf("A", "B", "C")
.flatMapConcat { value ->
flow {
for (i in 1..2) {
emit("${value}$i")
delay(100) // 模拟耗时
}
}
}
.collect { println(it) }
// 输出:A1 A2 B1 B2 C1 C2(严格顺序,间隔100ms)
使用场景:数据依赖性强,下游必须按上游顺序处理,例如:顺序写入日志文件、按序处理事务。
2. flatMapMerge——大家一起上
flatMapMerge 正好相反:上游每来一个元素,立即启动内层 Flow,所有内层 Flow 并发执行。输出顺序完全看谁跑得快,也就是“先完成先输出”。
我记得有一次做实时数据聚合,从三个不同 API 拉取行情数据。如果用 flatMapConcat,用户得等一分钟才看到所有报价,改用 flatMapMerge 后,哪个接口先返回就先显示,体验直接起飞。
并发控制:默认并发数是 DEFAULT_CONCURRENCY(等于 CPU 核数),也可以显式指定,比如 flatMapMerge(concurrency = 2) 最多同时跑 2 个内层 Flow。
代码示例:
flowOf("X", "Y", "Z")
.flatMapMerge(concurrency = 2) { value ->
flow {
emit("${value} start")
delay(200)
emit("${value} done")
}
}
.collect { println(it) }
// 可能输出(并发):X start, Y start, X done, Z start, Y done, Z done
// 注意:因为 concurrency=2,Z 必须等 X 或 Y 有空位才能启动
避坑指南:我曾经在无限制并发时(不设 concurrency)遇到过 OOM。上游元素多且每个内层 Flow 都吃内存,一定要设置合理的 concurrency。
3. flatMapLatest——只要最新的
这个操作符很有意思:当前一个内层 Flow 还没跑完、新元素又来了,直接取消旧的,只处理最新的那个。说白了就是“喜新厌旧”。
我刚学的时候觉得它有点暴力,但后来做搜索联想输入就发现它太香了:用户每敲一个字就发请求,如果前面请求还没回来,直接取消,只取最新的结果。这不正是我们需要的吗?
行为本质:当新元素到达时,立即 cancel 当前正在运行的内层 Flow(协程取消),然后启动新 Flow。如果旧 Flow 已经结束,则正常启动新 Flow。
代码示例:
flowOf("query1", "query2", "query3")
.flatMapLatest { query ->
flow {
emit("start $query")
delay(300) // 模拟网络请求
emit("result $query")
}
}
.collect { println(it) }
// 假设上游发射间隔 < 300ms,则输出可能为:
// start query1
// (收到 query2,取消 query1 的 Flow)
// start query2
// (收到 query3,取消 query2 的 Flow)
// start query3
// result query3 (只有最后一个完整执行)
注意事项:flatMapLatest 依赖协程的取消协作机制。如果内层 Flow 中的代码没有检查取消状态(比如纯 CPU 计算),可能无法及时取消。建议在耗时循环中加入 ensureActive() 或 yield()。
4. 三兄弟对比一览
光看文字可能还差点意思,我整理了一个表格,把关键指标列出来,方便你面试或做技术选型时直接参考。
| 特性 | flatMapConcat | flatMapMerge | flatMapLatest |
|---|---|---|---|
| 执行方式 | 串行 | 并发(可控制度) | 串行(丢弃旧流) |
| 输出顺序 | 严格按上游顺序 | 无序(按完成时间) | 只输出最后一个Flow的结果 |
| 并发数 | 1(无并发) | 可配置(默认CPU核数) | 1(但会取消旧任务) |
| 内存压力 | 低(一次一个Flow) | 较高(多个Flow共存) | 低(只保留最新) |
| 典型场景 | 事务、日志、依赖运算 | 并行网络请求、批量处理 | 搜索联想、实时传感器、UI刷新 |
5. 经验总结与思考
说了这么多,我再用几句话总结一下个人心得:
- 需要严格顺序时,无脑选 flatMapConcat。但要注意性能瓶颈——如果内层 Flow 都很慢,下游会被阻塞。
- 想提速且不关心顺序时,用 flatMapMerge。记得设置合理的 concurrency,否则容易打满线程池或撑爆内存。
- 场景天然适合“只取最新”时,优先考虑 flatMapLatest。它比手动 cancel 再启动要优雅得多。
我曾经在一个图片加载库中犯过错误:用了 flatMapConcat 去并发加载,结果用户快速滑动列表时,图片加载顺序完全错位。后来换成 flatMapLatest 才解决——每次只加载当前可见项的最新请求,旧请求自动取消,内存和 CPU 都省了。
一句话口诀:排队的用 concat,并发的用 merge,喜新厌旧的用 latest。实战中多搭配 catch 和 retry 使用,效果更稳。
好了,这三个操作符的底层原理其实都一样——都是将 Flow<Flow<T>> 展平成 Flow<T>,区别只在于内部协程的调度策略和取消时机。理解这个本质,你就能自己推导出它们的行为了。