在[第一部分]中,我们构建了“空气压缩机”(collect 函数)来启动气流并让活塞动起来。但说实话——如果你直接从压缩机向喷枪喷射原始空气,你的喷涂工作会变得潮湿、脏乱,且压力不均。

要进行专业的喷漆工作,你需要给软管加上附件。

在 Kotlin Flows 中,这些附件被称为操作符(Operators)。它们在数据到达 UI(喷枪)之前,拦截并改变传输中的数据。

拆解:最常见的三个附件

让我们看看你在软管的三个最常见附件:

1. 湿气陷阱(filter)​

有时压缩机会随空气喷出小水滴。你肯定不希望水毁了你的喷漆工作。filter 操作符就像个筛子——它只允许满足特定条件的东西通过。

// 只让完全干燥的空气脉冲通过
airFlow.filter { air ->
    air.isDry == true
}

2. 油漆混合器(map)​

现在,你只有原始空气。但你想要蓝色油漆。map 操作符拦截原始空气,混入蓝色油漆,然后将其沿管线发送出去。它实际上是将数据类型从 A 转换到 B。

// 将 RawAir 转换为 BluePaint
airFlow.map { rawAir ->
    Paint(color = "Blue", pressure = rawAir.pressure)
}

3. 安全泄压阀(catch)​

如果压缩机爆炸或管道破裂怎么办?在代码中,这就是异常(比如网络崩溃)。如果你没有安全阀,爆炸会传到喷枪并毁掉一切(导致应用崩溃)。catch 操作符安全地处理了爆炸。

airFlow
    .catch { exception ->
        println("Whoops! 管道破裂:异常")
        emit(BackupAir()) // 我们甚至可以发射一个回退值!
    }

图解心智模型

[压缩机] ---> (filter: 保持干燥) ---> (map: 添加油漆) ---> (catch: 安全保护)

引擎盖下:内部原理

Flow 中的操作符出奇地简单。它们不是神奇的黑盒;它们只是包裹在原始 Flow 上的包装器。当你应用 .map() 时,Kotlin 实际上会在你的原始 Flow 上创建一个全新的 Flow。当 UI 调用 .collect() 时,它实际上是在调用 map Flow 上的 collect,然后反过来调用原始 Flow 上的 collect。这是一条传递挂起函数的委托链!

“等等,但为什么?”环节

疑问:“嘿,如果我想过滤掉湿空气,为什么需要 .filter()?我不能在 UI 的 .collect() 块里直接放个 if 语句吗?”

回答:你可以,但你会让你的喷枪(代码)变得太复杂。在 Android 中,.collect() 块通常存在于你的 Activity、Fragment 或 Compose UI 中。如果你在 UI 里做过滤、数据格式化、错误处理,你的 UI 代码就会变成一团乱麻。操作符让你在 ViewModel 或存储库中完成所有繁重工作,让 UI 只接收最终的完美结果。保持喷枪简单!

疑问:“这些操作符和标准 Kotlin List 操作符有什么区别?它们也有 .map 和 .filter 啊!”

回答:时机!如果你在包含 1000 个项的列表上使用 .map,Kotlin 会一次性转换所有 1000 项,并在内存中创建一个全新的列表。这很重。Flow 操作符是顺序处理每个经过的项。项 1 经过过滤器,然后映射,然后进入 collect。接着项 2 再走一遍。它极其节省内存,因为从不创建巨大的辅助列表。

分层面试问题(预期内容)

等级 1:初级开发者(1-3 年经验)​

  • 问题:中间操作符(Intermediate Operator)和末端操作符(Terminal Operator)的区别是什么?
  • 如何回答:“中间操作符(如 map、filter、onEach)修改 Flow 但不启动它。它们只返回另一个 Flow。末端操作符(如 collect、toList、first)实际触发拉取,启动 Flow 的执行。”

等级 2:中级开发者(4-5 年经验)​

  • 问题:.catch() 操作符在 Flow 中如何工作?它会捕获 .collect() 块内发生的异常吗?
  • 如何回答:“不,它不会!catch 操作符只捕获上游(生产者端)发生的异常。它严格向下游(在 collect 块内部)生效,catch 操作符对 Flow 本身完全视而不见,除非用标准 try-catch 块包裹,否则应用会崩溃。”

等级 3:高级/架构师​

  • 问题:你正在构建一个搜索栏。用户打字很快。你如何使用 flatMapConcat、flatMapMerge 和 flatMapLatest?它们有何不同?
  • 如何回答:
    • “flatMapConcat 顺序处理一切。它等第一个搜索完成再开始第二个。对 UI 来说很糟糕。”
    • “flatMapMerge 并发处理它们。你会同时收到多个搜索结果,可能乱序。对搜索栏不好。”
    • “flatMapLatest 才是我们想要的。如果用户输入 ‘A’,它启动网络调用。如果在 ‘A’ 完成前输入 ‘B’,它取消 ‘A’ 的请求只处理 ‘B’。它防止竞态条件,通过丢弃过期请求节省带宽。”