
前言
元婴初成,你已能创造 Flow,感知冷流的懒加载韵律。你能用 flow {} 发射数据,用 collect 收集结果。但此刻的 Flow,还只是一条笔直的管道------数据从入口流入,原封不动地从出口流出。
真实世界的需求远比这复杂:
- 后端返回的用户列表里有
100条数据,我只需要名字包含'张'的前5条。- 搜索接口返回的
JSON里嵌套了太多无用字段,我只想提取其中的id和title。- 用户疯狂点击刷新,我只想处理最后一次请求的结果,之前的全扔掉。
- 网络请求可能失败,我想失败后自动重试
3次。
如果每遇到一个需求都要去修改 flow {} 块的内部代码,Flow 就失去了它作为响应式框架的优雅。
Flow 的真正威力,藏在一系列中间操作符 里。它们就像流水线上的工位,每个工位对数据做一点加工,最后输出成品。你只需要像搭积木一样,把这些操作符串联起来,就能构建出复杂的数据处理管道。
本讲是元婴境的中阶修炼。你将:
- 掌握
map、filter、transform等核心变换操作符。 - 彻底搞懂
flatMapConcat、flatMapMerge、flatMapLatest三兄弟的区别。 - 学会用
catch优雅处理 Flow 中的异常。 - 理解
onCompletion、onStart、onEach等生命周期操作符。
准备好让你的数据流变幻莫测了吗?我们开始。
操千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意。
操作符全景图:Flow 管道的积木块
Flow 的操作符分为两大类:
- 中间操作符 :作用于 Flow,返回一个新的 Flow。它们是懒加载 的,仅仅声明了数据处理逻辑,在
collect时才真正执行。 - 终端操作符 :触发 Flow 开始执行的挂起函数,如
collect、toList、first。
本讲重点讲解中间操作符。它们可以被无限串联,每个操作符只做一件事,符合单一职责原则。
基础变换三兄弟:map、filter、transform
map:一对一变换
map 是最常用的操作符。它接收上游的每一个值,应用一个变换函数,将结果发射给下游。
kotlin
flow {
emit(1)
emit(2)
emit(3)
}.map { value ->
"Number: $value"
}.collect { string ->
println(string)
}
// 输出:
// Number: 1
// Number: 2
// Number: 3
map 像流水线上的贴标机------每个经过的产品都被贴上一张标签,数量不变。
filter:条件过滤
filter 根据条件决定哪些值可以继续向下游传递。不满足条件的值会被丢弃。
kotlin
flow {
emit(1)
emit(2)
emit(3)
emit(4)
}.filter { value ->
value % 2 == 0 // 只保留偶数
}.collect { even ->
println(even)
}
// 输出:
// 2
// 4
filter 像质检员------合格的产品放行,不合格的剔除。
transform:万能变换
map 和 filter 的局限性在于它们都是一对一 或一对零 的变换。如果你需要一对多 (例如将一个字符串拆分为多个字符发射),就需要 transform。
kotlin
flow {
emit("Hello")
emit("World")
}.transform { word ->
// 可以多次 emit,甚至不 emit
word.forEach { char ->
emit(char.toString())
}
}.collect { char ->
println(char)
}
// 输出:H, e, l, l, o, W, o, r, l, d
transform 是最灵活的中间操作符,理论上可以替代任何其他操作符。但它代码量稍多,能用 map/filter 时优先用它们。
flatMap 家族:流的扁平化魔法
这是 Flow 操作符中最容易混淆,也最强大的部分。
当你有一个 Flow,其中的每个值本身又会产生一个新的 Flow 时,你就需要 flatMap 来将"流中流"扁平化为单一流。
flatMapConcat:顺序拼接
flatMapConcat 将内部 Flow 按顺序拼接。它会等待第一个内部 Flow 完全结束后,再开始收集第二个。
kotlin
fun getFlow(id: Int): Flow<String> = flow {
emit("$id: A")
delay(100)
emit("$id: B")
}
flow {
emit(1)
emit(2)
}.flatMapConcat { id ->
getFlow(id)
}.collect { value ->
println(value)
}
// 输出顺序(严格):
// 1: A
// 1: B
// 2: A
// 2: B
flatMapMerge:并发合并
flatMapMerge 会同时收集 所有内部 Flow,并将它们发射的值交错 发送给下游。你可以通过 concurrency 参数限制最大并发数。
kotlin
flow {
emit(1)
emit(2)
}.flatMapMerge { id ->
getFlow(id)
}.collect { value ->
println(value)
}
// 可能的输出(交错):
// 1: A
// 2: A
// 1: B
// 2: B
适用场景:需要并发执行多个网络请求,且不关心结果顺序时。
flatMapLatest:只取最新
flatMapLatest 的行为是:当上游发射新值时,如果上一个内部 Flow 还未完成,则取消它,只收集最新的内部 Flow。
这是实现实时搜索的关键操作符。
kotlin
flow {
emit("K") // 被取消
delay(50)
emit("Kot") // 被取消
delay(50)
emit("Kotlin") // 最终只收集这个
}.flatMapLatest { query ->
searchApi(query) // 返回 Flow
}.collect { result ->
println("搜索结果:$result")
}
```mermaid
flatMap 三兄弟对比表
| 操作符 | 行为 | 内部流执行顺序 | 适用场景 |
|---|---|---|---|
flatMapConcat |
顺序拼接 | 严格串行 | 顺序敏感的批量操作 |
flatMapMerge |
并发合并 | 并发,交错 | 并发请求,不关心顺序 |
flatMapLatest |
只取最新 | 新值到来时取消旧流 | 实时搜索、实时刷新 |
异常处理:catch 操作符
在 Flow 的世界里,异常处理有两种方式:
- 在
collect处用try-catch(终端处理)。 - 在管道中用
catch操作符(中间处理)。
catch 操作符可以捕获上游发生的任何异常,并允许你发射替代值或重新抛出。
kotlin
flow {
emit(1)
throw RuntimeException("炸了")
emit(2) // 不会执行
}.catch { e ->
println("捕获异常:${e.message}")
emit(-1) // 发射替代值
}.collect { value ->
println(value)
}
// 输出:
// 1
// 捕获异常:炸了
// -1
关键特性:
catch只能捕获上游 的异常。它无法捕获下游(如collect块内部)的异常。- 你可以在
catch中通过emit发射替代值,让流继续。
生命周期操作符:onStart、onEach、onCompletion
这些操作符让你能在 Flow 的各个生命周期节点插入副作用,例如显示/隐藏 Loading、打印日志。
| 操作符 | 触发时机 | 典型用途 |
|---|---|---|
onStart |
在 Flow 开始被收集之前 | 显示 Loading 动画 |
onEach |
每发射一个值之前 | 日志记录、埋点 |
onCompletion |
Flow 完成或异常结束之后 | 隐藏 Loading、资源清理 |
kotlin
flow {
delay(500)
emit(1)
emit(2)
}.onStart {
println("开始加载...")
// 可以 emit 额外值,例如 emit("Loading")
}.onEach { value ->
println("即将发射:$value")
}.onCompletion { cause ->
if (cause == null) {
println("流正常完成")
} else {
println("流异常结束:${cause.message}")
}
}.collect { value ->
println("收集到:$value")
}
// 输出:
// 开始加载...
// 即将发射:1
// 收集到:1
// 即将发射:2
// 收集到:2
// 流正常完成
实战:用 Flow 操作符实现优雅的搜索
让我们将本讲学到的操作符串联起来,实现一个完整的搜索功能。
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class SearchViewModel(
private val searchRepo: SearchRepository
) : ViewModel() {
private val queryFlow = MutableStateFlow("")
var uiState by mutableStateOf(SearchUiState())
private set
init {
viewModelScope.launch {
queryFlow
.debounce(300) // 去抖动
.filter { query ->
query.length >= 2 || query.isEmpty() // 空或至少2字符
}
.distinctUntilChanged() // 去重
.flatMapLatest { query ->
if (query.isEmpty()) {
flow { emit(emptyList()) } // 空查询直接返回空
} else {
// 开始搜索,先显示 Loading
searchFlow(query)
.onStart { uiState = uiState.copy(isLoading = true) }
.onCompletion { uiState = uiState.copy(isLoading = false) }
.catch { e -> emit(emptyList()) } // 异常时返回空
}
}
.collect { results ->
uiState = uiState.copy(results = results)
}
}
}
private fun searchFlow(query: String): Flow<List<String>> = flow {
// 模拟网络请求
delay(500)
emit(searchRepo.search(query))
}
fun onQueryChanged(newQuery: String) {
queryFlow.value = newQuery
}
}
data class SearchUiState(
val results: List<String> = emptyList(),
val isLoading: Boolean = false
)
常见错误与避坑指南
错误 1:在 map 中执行耗时操作但不使用挂起函数
kotlin
// ❌ 错误:map 内部不能直接调用挂起函数(除非使用特殊操作符)
flow.map { value ->
delay(100) // 编译错误!map 的 lambda 不是 suspend
value * 2
}
正确做法 :map 的 lambda 是普通函数,如果需要挂起操作,应使用 transform 或 .map { ... } 内部调用挂起函数(实际上 Flow 的 map 从 kotlinx.coroutines 1.3.0 开始支持内联挂起,但明确语义更好)。
kotlin
// ✅ 使用 transform
flow.transform { value ->
delay(100)
emit(value * 2)
}
错误 2:混淆 flatMap 家族的使用场景
kotlin
// ❌ 用 flatMapConcat 做实时搜索------用户快速输入时,旧请求不会取消,浪费资源
queryFlow.flatMapConcat { query -> searchFlow(query) }
正确做法 :实时搜索必须用 flatMapLatest 以取消旧请求。
错误 3:在 catch 中试图捕获下游异常
kotlin
flow { emit(1) }
.catch { e -> println("捕获不到下游异常") }
.collect { value ->
throw RuntimeException("collect 中的异常")
}
// catch 不会触发!
正确做法 :下游异常需在 collect 处用 try-catch。
最佳实践
-
优先使用专用操作符 :能用
map/filter就不用transform,代码更简洁、语义更清晰。 -
用
flatMapLatest处理用户交互触发的新请求:自动取消旧请求,避免资源浪费和状态错乱。 -
用
catch统一处理上游异常 :保持collect处代码干净。 -
用
onStart/onCompletion管理 UI 状态:将 Loading 逻辑封装在 Flow 管道内,而不是分散在 ViewModel 各处。 -
操作符顺序敏感 :例如
catch只能捕获上游,放在管道开头和结尾效果完全不同。通常将catch放在靠近可能出错的 Flow 之后。 -
调试时使用
onEach打印日志:观察数据在管道中的流动。
总结与下回预告
恭喜,你已经掌握了 Flow 操作符的精髓,元婴境中阶修炼完成!
本讲核心收获:
map/filter/transform实现基础数据变换。flatMapConcat顺序拼接、flatMapMerge并发合并、flatMapLatest只取最新。catch捕获上游异常,onStart/onEach/onCompletion管理生命周期。- Flow 操作符的串联构建出声明式的数据处理管道。
在下一讲 【元婴境·后阶】 中,我们将学习 Flow 的背压(Backpressure) 处理机制。届时你会明白:
- 当生产者速度远快于消费者时,Flow 如何避免数据堆积?
buffer、conflate、collectLatest三种背压策略的区别与适用场景。- 如何在 Android 中处理高频传感器数据?
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 元婴境 · 中阶 | 1、map/filter/transform 变换三法 2、flatMap 三兄弟真解 3、catch/onCompletion 生命周期术 |
当前进度 :50% 修为 :500/1000 下一突破 :[元婴境 · 后阶] (需领悟:buffer、conflate、collectLatest 背压处理三法) |
操作符是流水线上的工位。flatMapLatest是搜索的守护神------新值到来,旧请求立刻取消。 |
【本讲思考题】
1、表象题:以下代码的输出是什么?
kotlin
flow {
emit(1)
emit(2)
}.flatMapConcat { value ->
flow {
emit("$value-A")
emit("$value-B")
}
}.collect { println(it) }
2、场景题:你有一个显示股票实时价格的界面,价格每秒更新一次。当用户点击"刷新"按钮时,你希望立即发起一次请求获取最新价格,并取消之前还在运行的定时刷新流。应该用哪个 flatMap 操作符?写出核心代码。
3、原理题 :flatMapLatest 内部是如何实现"新值到来时取消旧流"的?请结合 transformLatest 或源码简述其实现机制。
道友,元婴境的最后一道关隘已在眼前。掌握了背压处理,你就能让 Flow 在极端数据洪流中稳如磐石。元婴境·后阶见。
欢迎一键四连 (
关注+点赞+收藏+评论)