Flow 异常:26. 基础异常捕获

本篇解析 example-flow-26.kt。学习如何处理 Flow 收集过程中的异常。

1. 核心概念:try-catch 块

Flow 的收集过程(collect)本质上是在协程中运行的。因此,你可以像处理普通挂起函数一样,使用传统的 try-catch 来包裹 collect

2. 代码解析

1
2
3
4
5
6
7
8
try {
simple().collect { value ->
println(value)
check(value <= 1) { "Collected $value" } // 模拟处理过程中的逻辑异常
}
} catch (e: Throwable) {
println("Caught $e") // 能够成功捕获到来自 collect 内部的异常
}

3. 开发者感悟

这种捕获方式是最直观的。它不仅能捕获发射端(emit)抛出的异常,也能捕获收集端(collect 逻辑块)抛出的异常。在简单的业务场景下,这是最可靠的选择。

Flow 异常:27. 操作符中的异常捕获

本篇解析 example-flow-27.kt。探讨 Flow 中各阶段异常的统一捕获。

1. 核心概念:透明性与一致性

Flow 的设计保证了异常处理的透明性。无论异常发生在:

  1. 发射端flow { ... } 构建器内部)。
  2. 转换操作符map, filter, transform 内部)。
  3. 收集端collect 内部)。

都可以使用包裹在 collect 外层的 try-catch 块来统一捕获。

2. 代码解析

1
2
3
4
5
try {
simple().collect { value -> println(value) }
} catch (e: Throwable) {
println("Caught $e")
}
  • 在这个例子中:异常发生在 map 操作符的 check 逻辑中。
  • 结果:即使异常发生在流的中间处理阶段,外层的 try-catch 依然能够感知并捕获它。

3. 开发者感悟

这种“一处捕获,全线生效”的机制极大地降低了异步异常处理的复杂度。你不需要在每个 map 里写 try-catch。只需在业务的最外层(消费端)统一处理即可。这保持了业务逻辑的整洁。

Flow 异常:28. Catch 声明式捕获

本篇解析 example-flow-28.kt。学习 Flow 提供的专门用于异常处理的操作符。

1. 核心操作符:catch

catch 操作符用于捕获并处理流中的异常。

特点:

  • 异常透明性:它只捕获上游发生的异常。
  • 恢复能力:在 catch 块中,你可以选择:
    1. 使用 emit 发射一个默认值或错误提示。
    2. 重新抛出异常。
    3. 记录日志并停止流。

2. 代码解析

1
2
3
simple()
.catch { e -> emit("Caught $e") } // 捕获上游异常并转换成一个正常的发射值
.collect { value -> println(value) }
  • 结果:当上游 map 报错时,catch 拦截了它,并发射了一个字符串 “Caught…”。收集端(collect)会像接收普通数据一样接收到这个字符串,程序不会崩溃。

3. 开发者感悟

catch 操作符让异常处理变得非常具有“流”的风格。它将错误处理也变成了数据流的一部分,这在构建响应式 UI 时非常优雅。

Flow 异常:29. Catch 操作符的局限性

本篇解析 example-flow-29.kt。探讨 catch 在处理下游异常时的局限。

1. 核心规则:仅捕获上游异常

catch 操作符遵循异常透明性原则。

规则解析:

  • 它只能捕获发生在它之前(上游)的操作符或构建器的异常。
  • 无法捕获发生在它之后(下游)的异常,特别是 collect 块内部的异常。

2. 代码解析

1
2
3
4
5
6
simple()
.catch { e -> println("Caught $e") } // 这里捕获不到 collect 里的异常
.collect { value ->
check(value <= 1) { "Collected $value" } // 这里抛出的异常会逃逸
println(value)
}
  • 结果:如果 collect 中发生异常,程序依然会因为未捕获异常而崩溃。

3. 开发者感悟

这是一个非常容易踩的坑。很多开发者以为加了 .catch { ... } 就万事大吉了。其实它只保证了“生产”和“加工”阶段的安全,不保证“消费”阶段的安全。如果你需要处理消费阶段的异常,请参考下一节的“声明式捕捉”。

Flow 异常:30. 声明式捕捉与收集异常

本篇解析 example-flow-30.kt。学习一种能覆盖全流程异常处理的优雅写法。

1. 核心技巧:onEach + catch

为了让 catch 操作符能够捕获到原本在 collect 中的异常,我们可以通过以下重构方式:

重构步骤:

  1. collect { ... } 中的业务逻辑移动到 onEach { ... } 操作符中。
  2. 确保 onEach 位于 catch 之前(上游)
  3. 最后使用无参的 collect() 启动流。

2. 代码解析

1
2
3
4
5
6
7
simple()
.onEach { value ->
check(value <= 1) { "Collected $value" } // 业务逻辑在这里
println(value)
}
.catch { e -> println("Caught $e") } // 现在它可以捕获到上面 check 抛出的异常了
.collect()
  • 结果:这种链式调用非常整洁,所有的异常(生产、加工、消费)都被统一在 catch 块中处理了。

3. 开发者感悟

这是我最推崇的一种 Flow 异常处理模式。它避免了嵌套 try-catch 块,让数据流处理变得纯粹、声明式。在 Android 的 ViewModelRepository 中,这种模式能极大地提高代码的可维护性。

Flow 完成:31. 命令式完成 finally

本篇解析 example-flow-31.kt。探讨如何感知 Flow 的结束。

1. 核心概念:try-finally

就像处理异常一样,你可以使用传统的 try-finally 块来在 Flow 结束时执行特定逻辑。

2. 代码解析

1
2
3
4
5
try {
simple().collect { value -> println(value) }
} finally {
println("Done") // 无论流是正常结束还是报错结束,这里都会执行
}

3. 开发者感悟

这是最基础的资源清理方式。如果你在收集 Flow 时开启了某些外部资源(如传感器、进度条),在 finally 块中关闭它们是最稳妥的。

Flow 完成:32. onCompletion 声明式完成

本篇解析 example-flow-32.kt。学习如何使用操作符来处理 Flow 的结束。

1. 核心操作符:onCompletion

onCompletion 是一个声明式的操作符,用于在流收集完成时执行逻辑。

与 finally 的区别:

  • 链式调用:它作为一个操作符存在,使代码流更加统一。
  • 异常感知:它能感知到流是正常结束还是由于异常结束。

2. 代码解析

1
2
3
simple()
.onCompletion { println("Done") } // 流结束时触发
.collect { value -> println(value) }

3. 开发者感悟

onCompletiontry-finally 更符合响应式编程的习惯。它常用于在数据流结束后隐藏 UI 上的加载动画或进度条。

Flow 完成:33. onCompletion 异常感知

本篇解析 example-flow-33.kt。探讨如何在完成回调中识别异常。

1. 核心特性:Throwable 参数

onCompletion 操作符提供了一个可选的 Throwable? 参数。

参数说明:

  • cause == null:表示流是正常处理完毕(所有元素发射并收集完成)。
  • cause != null:表示流是因为抛出了异常而提前终止。

2. 代码解析

1
2
3
4
5
6
simple()
.onCompletion { cause ->
if (cause != null) println("Flow completed exceptionally")
}
.catch { cause -> println("Caught exception") }
.collect { value -> println(value) }
  • 注意onCompletion 仅仅是“感知”到了异常。它不会捕获异常。异常依然会向下游传播,直到被 catchtry-catch 处理。

3. 开发者感悟

onCompletion 就像是一个监控摄像头,它只负责记录流是为什么结束的。如果你需要处理错误(如显示错误占位图),请配合 catch 使用。

Flow 完成:34. onCompletion 观察所有异常

本篇解析 example-flow-34.kt。探讨 onCompletion 在感知异常方面的优势。

1. 核心特性:全方位观察

catch 操作符只能捕获上游异常不同,onCompletion 能够观察到所有导致流结束的异常(包括上游、中间操作符以及下游 collect 里的异常)。

2. 代码解析

1
2
3
4
5
6
simple()
.onCompletion { cause -> println("Flow completed with $cause") }
.collect { value ->
check(value <= 1) { "Collected $value" } // 这里抛出的异常也会被 onCompletion 看到
println(value)
}
  • 注意:虽然它能看到下游的异常,但它依然不捕获它。程序仍会因为这个异常而报错。

3. 开发者感悟

onCompletion 就像是一个“黑匣子”飞行记录仪。无论流是因为什么原因坠毁的,它都能把最后的异常状态记录下来。这对于问题排查和日志分析非常有价值。

Flow 监听:35. Collect 的阻塞性

本篇解析 example-flow-35.kt。探讨如何在不阻塞当前协程的情况下监听 Flow。

1. 核心问题:Collect 是挂起并等待的

collect 是一个末端操作符,它会挂起当前协程,直到 Flow 所有的值都被收集完成。

代码解析

1
2
3
4
events()
.onEach { event -> println("Event: $event") }
.collect() // 这里会一直挂起等待
println("Done") // 只有等 flow 结束(通常是 300ms 后),才会打印 "Done"

2. 开发者感悟

如果你在处理 UI 事件流(比如按钮点击或 Socket 消息),直接使用 collect 会导致其后的代码无法执行。这就像是你守在一个窗口前,不等到人(流结束)就不走开。为了解决这个问题,我们需要 launchIn