Flow在Android中的完整落地:异常、重试与生命周期
一条Flow链路只有同时处理失败边界和页面生命周期,才算真正落地。retryWhen、catch和onCompletion决定上游失败后是否重来、如何降级以及怎样观察结束;repeatOnLifecycle和collectAsStateWithLifecycle则决定页面在什么状态下消费数据。
这两组机制共同约束完整运行过程:数据源何时启动,异常影响哪一段,重试会重复执行什么,页面不可见后收集是否停止,以及再次可见时上游是否重新建立。
内容摘要
retryWhen根据异常和次数决定是否重新执行它前面的上游。catch只捕获前方上游异常,不能捕获后方collect代码块的失败。onCompletion观察正常完成、异常和取消,但不会自动消费异常。- Fragment使用
viewLifecycleOwner.lifecycleScope + repeatOnLifecycle(STARTED)约束View存在和可见周期。 - Compose状态使用
collectAsStateWithLifecycle,一次性副作用在LaunchedEffect中按生命周期收集。

一、操作符位置决定异常作用范围
scss
repository.observeTasks()
// 把仓库数据转换成页面可直接渲染的状态
.map(::toUiState)
.catch { error ->
// catch 只处理它前方上游抛出的异常
emit(TaskUiState.Error(error))
}
// render 位于 catch 下游,其内部异常不会被前面的 catch 捕获
.collect(::render)
相对于catch,数据源和map位于上游,collect位于下游。因此:
arduino
数据源或map抛出异常 → catch可以捕获
catch后方操作符异常 → 当前catch无法捕获
collect内部异常 → 当前catch无法捕获
如果render自身可能失败,需要在收集代码块内处理,或在启动收集的协程边界统一观察。把catch放在链尾并不意味着它可以包住整个调用栈。
catch可以选择三种常见策略:
go
.catch { error ->
// 上游失败后可以记录、降级,也可以选择继续传播
logger.error("load tasks failed", error) // 记录
emit(TaskUiState.Error(error)) // 降级
// 或 throw error // 继续传播
}
降级数据必须符合下游类型和业务含义。把任何失败都替换成空列表,会混淆"确实没有数据"和"数据加载失败"。
本节小结
Flow异常边界由操作符在链中的位置决定。catch只能处理前方上游,错误状态也应保留失败语义,不能用空数据掩盖真实故障。
二、重试会重新执行整段上游
scss
repository.observeTasks()
.retryWhen { error, attempt ->
// 只对临时 IO 故障进行有限重试;attempt 从 0 开始
val canRetry =
error is IOException && attempt < 2
if (canRetry) {
// 每次失败后增加等待时间,避免立即连续冲击数据源
delay(1_000L * (attempt + 1))
}
// true 会重新执行 retryWhen 前面的整个上游
canRetry
}
.catch { error ->
// 重试耗尽或错误不可重试时,转换成最终错误状态
emit(TaskUiState.Error(error))
}
attempt从0开始。attempt < 2表示首次失败后最多重试两次,因此上游最多执行三次。
retryWhen返回true时,重新启动它前面的整个上游,而不是只重跑抛异常的那一行:
重新订阅数据源
→ 重新执行上游转换
→ 再次到达失败点或成功发射
这意味着上游中的副作用也可能重复发生。查询操作遇到临时网络故障可以有限重试;创建订单、支付、提交表单等写操作必须先建立端到端幂等。
客户端通常为同一次业务操作复用相同幂等键,服务端依据该键返回既有结果而不是重复执行。只有客户端生成标识而服务端不识别,不能构成真正幂等。
重试条件还应区分错误类型:
网络中断、超时、部分服务端错误 → 可以有限重试
参数错误、权限不足、业务校验失败 → 通常不应重试
取消 → 应继续传播,不转成重试
本节小结
retryWhen重启整个上游,因此重试次数、退避策略、错误分类和副作用幂等必须一起设计。重试不是通用容错开关。
三、onCompletion统一观察结束原因
rust
flow
.onCompletion { cause ->
// onCompletion 观察所有结束路径,但不会自动消费异常
when (cause) {
// cause 为空表示正常完成
null -> logger.info("flow completed")
is CancellationException ->
// 生命周期取消属于正常结束路径,不记录为业务故障
logger.info("flow cancelled")
else ->
// 非取消异常仍会继续向下游或调用方传播
logger.error("flow failed", cause)
}
}
.collect(::render)
onCompletion类似声明式finally,无论正常结束、异常结束还是取消都会执行:
yaml
cause == null → 正常完成
cause != null → 异常或取消
它可以观察和记录结束原因,却不会像catch一样自动消费上游异常。观察完成与处理失败是两项不同职责。
操作符顺序也会改变可见结果。常见链路是:
arduino
数据源
→ 数据转换
→ retryWhen
→ catch
→ onCompletion
→ collect
如果catch把异常转换成降级数据并正常结束,位于其后的onCompletion看到的cause可能是null。因此监控原始失败时,应在catch中记录,不能只依赖末尾完成回调。
本节小结
retryWhen决定是否重来,catch决定失败后如何处理,onCompletion观察最终如何结束。三者必须按需要观察的范围排列。
四、Fragment收集需要两层生命周期边界
javascript
viewLifecycleOwner.lifecycleScope.launch {
// 外层任务绑定当前 Fragment View,onDestroyView 时整体取消
viewLifecycleOwner.repeatOnLifecycle(
Lifecycle.State.STARTED
) {
// 达到 STARTED 时启动,低于 STARTED 时取消本次收集
viewModel.uiState.collect(::render)
}
}
这段结构同时约束两个不同周期:
scss
viewLifecycleOwner.lifecycleScope
→ 当前这套Fragment View销毁时,取消整棵收集任务树
repeatOnLifecycle(STARTED)
→ View达到STARTED时启动收集
→ 低于STARTED时取消本次收集
→ 再次达到STARTED时重新启动
Fragment对象可能仍在返回栈中,但它的View已经销毁。使用Fragment自身的lifecycleScope更新View,可能让旧任务继续引用失效的Binding;UI收集应绑定viewLifecycleOwner。
只使用viewLifecycleOwner.lifecycleScope.launch也不完整。它会在onDestroyView时取消,却不会在页面进入STOPPED、暂时不可见时自动停止长期收集。
多条Flow需要分别启动收集
scss
viewLifecycleOwner.repeatOnLifecycle(
Lifecycle.State.STARTED
) {
// 每条长期 Flow 都要放进独立子协程并发收集
launch {
viewModel.uiState.collect(::render)
}
launch {
// 如果顺序写在第一个 collect 后面,这条事件流不会启动
viewModel.events.collect(::handleEvent)
}
}
两个长期collect如果顺序书写,第一个通常不会结束,第二个也就无法启动。分别launch可以并发消费,并且仍处于同一个结构化作用域中;页面低于目标状态时,两个子任务一起取消。
单条Flow的简单场景可以使用flowWithLifecycle。多条Flow或需要把多个收集任务放在同一生命周期区间时,repeatOnLifecycle结构更清晰。
工程场景:页面旋转导致上游反复建立
假设Repository暴露一条冷Flow,每次收集都会重新注册数据库观察者或建立设备连接:
kotlin
fun observeDevice(): Flow<DeviceState> = callbackFlow {
// 每次收集冷 Flow 都会创建并注册一份新监听器
val listener = DeviceListener { state ->
// 回调线程使用非挂起 trySend 把状态送入 Flow
trySend(state)
}
// 收集开始时建立外部资源连接
deviceClient.register(listener)
awaitClose {
// 收集取消时解除监听,避免旧 View 继续接收数据
deviceClient.unregister(listener)
}
}
Fragment直接收集这条冷流时,页面旋转会经历:
sql
旧View低于STARTED
→ repeatOnLifecycle取消收集
→ awaitClose注销监听器
→ 新View达到STARTED
→ 再次收集冷Flow
→ 重新注册监听器
取消和重建本身是正确行为,但昂贵连接可能在短时间内频繁断开和恢复。可以在ViewModel中把冷流共享为状态:
ini
val deviceState: StateFlow<DeviceState> =
repository.observeDevice()
.stateIn(
// 共享任务存活范围绑定 ViewModel,而不是某一套 Fragment View
scope = viewModelScope,
started =
// 短暂旋转重建期间保留上游连接 5 秒
SharingStarted.WhileSubscribed(5_000),
// 连接返回首个状态前提供明确初始值
initialValue = DeviceState.Disconnected
)
页面短暂重建时,新订阅者通常会在5秒宽限期内出现,上游连接可以继续复用:
sql
旧View停止收集
→ 进入5秒停止宽限期
→ 新View开始收集
→ 取消停止计划
→ 继续使用同一份上游
如果页面离开超过宽限期,上游仍会停止并在下次订阅时重新建立。这个行为要求上游能够安全地重复注册和注销,不能把一次性副作用偷偷放进每次收集都会执行的冷流。
一次性网络提交、下单或写入操作也不应依赖页面collect触发。此类动作应由明确命令启动,结果进入状态存储,并通过幂等机制处理重复执行。生命周期重建可以恢复观察,但不应重新发起业务副作用。
这个场景需要同时理解三层边界:
sql
repeatOnLifecycle
→ 控制当前View何时消费
stateIn + WhileSubscribed
→ 控制多个View订阅如何共享上游
Repository数据源
→ 保证注册、注销和重启本身安全
本节小结
Fragment安全收集需要同时管理View是否存在和页面是否可见。viewLifecycleOwner解决对象边界,repeatOnLifecycle解决活跃区间。
五、Compose需要区分状态读取和一次性副作用
页面状态使用:
kotlin
@Composable
fun TaskScreen(viewModel: TaskViewModel) {
// 将 StateFlow 转换为 Compose State,并自动按 Lifecycle 启停收集
val state by viewModel.uiState
.collectAsStateWithLifecycle()
// state 变化后触发相关组合范围重新执行
TaskContent(state)
}
collectAsStateWithLifecycle()把Flow转换为Compose可观察状态,并按Lifecycle控制收集;默认最低活跃状态通常是STARTED。
导航、提示等一次性事件不应伪装成可重复渲染的当前状态。它们可以在LaunchedEffect中启动受组合生命周期管理的协程,并进一步按页面Lifecycle收集:
kotlin
@Composable
fun TaskRoute(viewModel: TaskViewModel) {
// 获取承载当前 Compose 页面所处的 LifecycleOwner
val lifecycleOwner = LocalLifecycleOwner.current
// Effect 离开组合或 key 变化时,内部协程会自动取消
LaunchedEffect(viewModel.events, lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(
Lifecycle.State.STARTED
) {
// 一次性事件只在页面可见期间处理
viewModel.events.collect(::handleEvent)
}
}
}
Composable离开组合时,LaunchedEffect中的协程会取消;键发生变化时,旧协程取消并按新键重新启动。
最低活跃状态应根据业务选择:
swift
CREATED → 创建后即可收集,即使页面不可见
STARTED → 页面可见时收集,适合大多数UI状态
RESUMED → 页面可见且可交互时收集
INITIALIZED不能作为这类生命周期收集API的有效最低状态。
本节小结
Compose中的状态进入重组系统,一次性事件进入副作用系统。二者都要受生命周期约束,但不能用同一种数据语义表达。
结语:把数据链路和页面生命周期接成闭环
一条完整的Android Flow链路可以归纳为:
arduino
数据源与转换
→ retryWhen控制有限重试
→ catch转换失败状态
→ onCompletion观察结束
→ StateFlow或SharedFlow承载数据语义
→ 生命周期感知收集
→ 页面渲染或执行副作用
失败处理保证链路能够解释"为什么结束",生命周期收集保证链路只在页面有效期间工作。两者缺少任何一侧,Flow都可能在异常时失去语义,或在页面不可见后继续消耗资源。