第2章 Kotlin Flow基础回顾:冷流与热流、StateFlow、SharedFlow、flowOn与catch
在深入缓存更新实现之前,必须掌握 Flow 的核心概念:冷流 (Cold Flow) 与 热流 (Hot Flow)。它们决定了数据的产生时机、订阅者的行为以及内存模型。Kotlin 协程库提供的 StateFlow 和 SharedFlow 是热流的标准实现,而 flowOn 和 catch 是构建健壮流水线的关键操作符。
2.1 冷流与热流:核心差异
冷流(如 flow { … } 构建的 Flow)的数据生产与订阅者绑定:每当有一个新的订阅者 collect,数据源就会重新执行一次。热流(如 StateFlow / SharedFlow)的数据生产独立于订阅者:数据在发射端持续产生,多个订阅者共享同一份数据流。
| 特性 | 冷流 (Cold Flow) | 热流 (Hot Flow) |
|---|---|---|
| 数据产生时机 | 每个 collect 触发一次 | 独立于订阅者,持续产生 |
| 订阅者数量 | 每个订阅者接收完整独立序列 | 所有订阅者共享同一个数据流 |
| 典型实现 | flow { … }, callbackFlow |
StateFlow, SharedFlow |
| 适用场景 | 一次性请求、独立计算任务 | 状态持有、事件广播、观察者模式 |
2.2 StateFlow:状态持有型热流
StateFlow 是热流的一种,它始终持有一个当前值,新订阅者 collect 时会立刻获取该值。它的数据源通过 MutableStateFlow 手动更新。
// 定义
val stateFlow = MutableStateFlow("初始值")
// 发射更新
stateFlow.value = "新值"
// 收集(在 ViewModel 中)
viewModelScope.launch {
stateFlow.collect { value ->
// 每次值变化时触发
}
}
提示:StateFlow 的
value 是线程安全的,可使用 compareAndSet 进行原子更新。适合用作 UI 状态持有容器。
2.3 SharedFlow:事件广播型热流
SharedFlow 不持有最新值(除非配置了 replay),它更适用于一次性事件的广播,如导航路由、Snackbar 消息。它支持 replay(新订阅者重播已发射的历史条数)、extraBufferCapacity 和 onBufferOverflow 策略。
// 创建 SharedFlow(默认 replay = 0,不重放)
val sharedFlow = MutableSharedFlow<Event>()
// 发射事件(挂起直到有缓冲空间)
viewModelScope.launch {
sharedFlow.emit(Event("login_success"))
}
// 收集事件
viewModelScope.launch {
sharedFlow.collect { event ->
// 处理事件
}
}
| 参数 | 说明 | 默认值 |
|---|---|---|
replay |
新订阅者收到最近 N 条已发射数据 | 0 |
extraBufferCapacity |
超出 replay 后的额外缓存槽位 | 0 |
onBufferOverflow |
缓存满时的策略(SUSPEND / DROP_OLDEST / DROP_LATEST) |
SUSPEND |
注意:SharedFlow 的
emit 是挂起函数,在缓存满时可能挂起协程。如果不想挂起,可使用 tryEmit(返回 Boolean 表示是否成功)。但在 UI 层面建议使用 StateFlow 或带适当缓冲的 SharedFlow。
2.4 上下文切换:flowOn
flowOn 用于改变 Flow 上游的协程上下文,常用于将数据生产逻辑切换到 Dispatchers.IO,而下游收集保持主线程。
flow {
// 上游运行在 Dispatchers.IO
emit(fetchDataFromNetwork())
}
.flowOn(Dispatchers.IO) // 切换上游
.collect { data ->
// 下游运行在 collect 所在线程(通常为主线程)
updateUI(data)
}
flowOn 只影响它之前的上游操作符,不会影响后续的操作。相同上下文重复调用 flowOn 会合并,但建议保持清晰。
2.5 异常处理:catch
catch 操作符捕获上游所有异常,并允许你恢复流量或发射兜底值。它位于 flowOn 下游时,可以捕获上游的异常。
flow {
emit(riskyOperation()) // 可能抛出异常
}
.flowOn(Dispatchers.IO)
.catch { e ->
// 捕获异常,可以发射默认值或重新抛出
emit(DefaultData)
// 如果不想继续,可以 throw e
}
.collect { data ->
// 正常处理
}
最佳实践:始终将
flowOn 放在 catch 之前,这样 catch 才能捕获上游异常;若先 catch 再 flowOn,则 catch 无法捕获来自 IO 上下文的异常。
2.6 常见陷阱与性能洞察
- 冷流重复执行:每个
collect都会重新触发冷流的整个生产逻辑,可能导致多次网络请求。应对:转用热流(StateFlow / SharedFlow)或使用shareIn/stateIn将其转为热流。 - StateFlow 丢失事件:当值相同时,StateFlow 不会通知订阅者(基于
equals去重)。若需要重复通知相同数据,应改用SharedFlow(replay = 1)或将数据包装为增量事件。 - SharedFlow 背压:如果消费者处理速度慢于发射速度且缓冲满,
emit会挂起。建议设置足够的extraBufferCapacity或使用DROP_OLDEST策略。 - flowOn 配合 catch 顺序:如前所述,注意操作符链顺序,不要颠倒。