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 是同步转换,不要在里面做耗时操作!如果需要异步转换,请用 mapLatest 或 flatMapLatest。

为什么不行?因为 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 帮了大忙,但前提是条件写对。

小技巧: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 做网络重试,配合指数退避策略,效果很好。

避坑指南:我曾经在 retry 里忘记加次数限制,结果网络挂了之后,应用一直在后台疯狂重试,直接把用户流量跑光了。嗯,现在任何 retry 我都会加上 maxRetries 参数。

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 个请求都应该取消,只保留最后一个。

核心建议:搜索、刷新这种“只需最新结果”的场景,无脑选 flatMapLatest。性能好,用户体验也棒。

3.8 终止操作符

Flow 的操作符都是冷操作,只有调用了终止操作符才会真正执行。常见的终止操作符有:

  • collect:最常用,逐个收集。
  • toList:把流里的所有元素收集成一个 List。
  • single:期望流里只有一个元素,多了会抛异常。
  • first:只取第一个元素,然后取消流。
flowOf(1, 2, 3)
    .toList() // 结果是 [1, 2, 3]
注意:toList 会把整个流都执行完,如果流是无限循环的,那就永远出不来了。一定要确定流的长度是有限的再用。

3.9 核心知识图谱

Flow 操作符家族(知识图谱) Flow 数据流 转换类 过滤类 组合类 终止类 map transform filter distinctUntilChanged combine/zip collect toList 进阶选择 flatMapConcat(顺序) | flatMapMerge(并发) | flatMapLatest(最新覆盖) 我在项目里的个人心法 取最新:flatMapLatest | 去重:distinctUntilChanged | 重试:retry + 指数退避 同步转换用 map,灵活多变用 transform,异步合并用 combine

3.10 小结:操作符的选择原则

说了这么多,最后总结几条我自己的经验:

  1. 能用 map 就别用 transform。map 更简单,也更容易理解。只有当你需要多次 emit 时才上 transform。
  2. 过滤操作尽量提前。把 filter 放在链式调用的前面,减少后续操作的数据量。
  3. 组合操作优先选 combine。zip 的配对模式容易丢失数据,除非你确定两个流长度相同。
  4. 终止操作要选对。不要动不动就 toList,除非你确定流会结束。

嗯,Flow 的操作符体系其实不复杂,关键是多用、多踩坑。你在实际项目里写过几个流之后,自然就知道哪个场景该用哪个了。