第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
适用场景 一次性请求、独立计算任务 状态持有、事件广播、观察者模式
冷流 vs 热流 冷流(Cold Flow) 数据发射器 消费者 A 消费者 B 热流(Hot Flow) 数据发射器 共享总线 消费者 C 消费者 D 冷流:每个订阅者独立收到完整数据流(如同“点播”)。 热流:所有订阅者共享同一数据流(如同“直播”)。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 顺序:如前所述,注意操作符链顺序,不要颠倒。