3. Flow操作符:map、filter、transform 归纳总结
好,咱们进入第三章。这一章专门聊 Flow 的操作符。
说实话,我刚接触 Flow 时,第一反应是:“这不就是 RxJava 换了个马甲吗?”嗯,确实有点像,但细看之后你会发现,Flow 的操作符设计得更简洁,也更符合 Kotlin 的协程气质。
我个人习惯把 Flow 操作符分成三大类:转换类、过滤类、辅助类。咱们一个个聊,顺带附上我在项目中踩过的坑。
3.1 转换操作符:map
map 是最基本的转换操作符。它的作用很简单,把流里的每个元素转换成另一个值。
flowOf(1, 2, 3)
.map { it * 2 }
.collect { println(it) }
// 输出:2, 4, 6
你想想看,这跟 RxJava 的 map 其实一模一样。但我曾经在项目里犯过一个低级错误——在 map 里做了网络请求。
为什么不行?因为 map 里的 lambda 是在当前协程里同步执行的。如果你在里面调用 suspend 函数,虽然不会报错,但会阻塞整个流。说白了,map 不是为异步准备的。
3.2 过滤操作符:filter
filter 就是字面意思,过滤掉不满足条件的元素。它接收一个返回 Boolean 的 lambda,为 true 就保留,为 false 就丢弃。
flowOf(1, 2, 3, 4, 5)
.filter { it % 2 == 0 }
.collect { println(it) }
// 输出:2, 4
我记得有一次处理用户列表,需要过滤出已激活的用户。UI 同学把“已激活”的状态码写成了 1,结果后端返回的是 "active"。嗯,filter 帮了大忙,但前提是条件写对。
3.3 转换操作符:transform
transform 比 map 灵活得多。它允许你发射任意数量的值,甚至可以发射 0 个。说白了,map 只能 1 对 1 转换,而 transform 可以 1 对多,或者 1 对 0。
flowOf(1, 2, 3)
.transform { value ->
emit(value)
emit(value * 10)
}
.collect { println(it) }
// 输出:1, 10, 2, 20, 3, 30
我在项目里用 transform 处理过个经典场景:分页加载。每页返回 20 条数据,但 UI 需要逐条展示。这时候 transform 配上 emit 逐个发射,优雅得很。
3.4 对比表格:map vs transform
| 属性 | map | transform |
|---|---|---|
| 输出数量 | 1 对 1 | 1 对 0..N |
| 异步能力 | 不支持 | 支持 emit 多次 |
| 典型场景 | 类型转换、数值运算 | 分页、缓存、组合多个数据源 |
| 是否可跳过 | 不能 | 可以(不调用 emit) |
3.5 其他常用操作符
除了 map、filter、transform,还有几个操作符我经常用,简单列一下:
- take:只取前 N 个元素。比如说你只关心前 5 条数据。
- drop:跳过前 N 个元素。翻页时很常见。
- distinctUntilChanged:连续重复的值只保留一次。UI 更新防抖利器。
- retry:失败后重试。配合网络请求再合适不过。
flowOf(1, 1, 2, 2, 3)
.distinctUntilChanged()
.collect { println(it) }
// 输出:1, 2, 3
我个人习惯在数据层用 retry 做网络重试,配合指数退避策略,效果很好。
3.6 组合操作符:combine、zip
当你需要把两个 Flow 合并成一个时,combine 和 zip 就派上用场了。
combine:两个流中任何一个有新数据时,都会用最新的值重新计算。适合做实时数据合并,比如用户名和头像。
zip:两个流一一配对,一个等另一个。适合做配对操作,比如同时请求两个接口,等两个都返回后再处理。
val flowA = flowOf("A1", "A2", "A3")
val flowB = flowOf("B1", "B2")
flowA.zip(flowB) { a, b -> "$a + $b" }
.collect { println(it) }
// 输出:A1 + B1, A2 + B2
注意 zip 以较短的流为准。上面 B 只有两个,所以只输出两次。
3.7 扁平化操作符:flatMapConcat、flatMapMerge、flatMapLatest
这几个是高手必备。
- flatMapConcat:顺序执行,一个完了再下一个。适合有先后依赖关系的场景。
- flatMapMerge:并发执行,同时处理多个流。适合无依赖的并行任务。
- flatMapLatest:最新覆盖旧的。新数据来了,旧的还没处理完就直接取消。
我个人最爱用 flatMapLatest,尤其是搜索联想功能。用户每输入一个字符,就发起搜索请求。如果用户快速输入了 5 个字符,前 4 个请求都应该取消,只保留最后一个。
3.8 终止操作符
Flow 的操作符都是冷操作,只有调用了终止操作符才会真正执行。常见的终止操作符有:
- collect:最常用,逐个收集。
- toList:把流里的所有元素收集成一个 List。
- single:期望流里只有一个元素,多了会抛异常。
- first:只取第一个元素,然后取消流。
flowOf(1, 2, 3)
.toList() // 结果是 [1, 2, 3]
3.9 核心知识图谱
3.10 小结:操作符的选择原则
说了这么多,最后总结几条我自己的经验:
- 能用 map 就别用 transform。map 更简单,也更容易理解。只有当你需要多次 emit 时才上 transform。
- 过滤操作尽量提前。把 filter 放在链式调用的前面,减少后续操作的数据量。
- 组合操作优先选 combine。zip 的配对模式容易丢失数据,除非你确定两个流长度相同。
- 终止操作要选对。不要动不动就 toList,除非你确定流会结束。
嗯,Flow 的操作符体系其实不复杂,关键是多用、多踩坑。你在实际项目里写过几个流之后,自然就知道哪个场景该用哪个了。