2. Kotlin Flow 基础回顾:Flow、StateFlow、SharedFlow 的特点与选择

在开始监听权限状态之前,必须先理解 Kotlin 协程生态中三种数据流的概念差异。Flow、StateFlow 与 SharedFlow 分别服务于不同的数据传播场景,选择错误会导致状态丢失、资源浪费或 UI 与业务层耦合。本章从冷/热流本质出发,对比三者的核心特性,并给出基于具体场景的选择策略。

Kotlin Flow 三种变体对比结构 Flow(冷流) 数据生产者 suspend 懒执行 collect 触发消费 每个订阅者独立执行 无状态 不保留中间值 cold ❄️ StateFlow(热流) 有初始值 构造时必传 最新状态缓存 只保留最后一个值 多个订阅者共享 免去重复计算 hot ☀️ SharedFlow(热流) 无初始值 可空类型可选 replay 重放 可配置重放数量 缓冲/extraBufferCapacity 防背压 hot ☀️ 冷流每次 collect 都重头执行;热流发射后即使无订阅者也会丢失(SharedFlow 通过 replay 做补偿)

2.1 Flow —— 冷流的标准实现

Flow 是协程中处理异步数据流的基本接口。它是一个 冷流:每次调用 collect 时,生产者的代码块都会从头执行。同一个 Flow 被多个收集者收集时,每个收集者得到独立的数据序列。

  • 典型创建方式:flow { ... }、flowOf(...)、集合扩展 .asFlow()。
  • 终止操作:collect、toList、first 等均为挂起函数。
  • 适用场景:一次性数据获取(如网络请求结果流),或者每个收集者需要独立计算的任务。
fun simpleFlow(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100)
        emit(i) // 发射数据
    }
}

// 每次 collect 都会打印 1,2,3
scope.launch { simpleFlow().collect { println(it) } }

2.2 StateFlow —— 状态持有者

StateFlow 是热流,继承自 SharedFlow,专门用于管理状态。它必须有一个初始值,并且始终缓存最新值——新订阅者会立即收到当前状态,不会重复之前的所有发射。

  • 不可变性:外部只暴露 StateFlow(只读),内部使用 MutableStateFlow 更新。
  • 去重:默认使用 equals 比较,只有值变化时才通知订阅者。
  • 典型用途:替代 LiveData 作为 ViewModel 的 UI 状态。
private val _permissionState = MutableStateFlow<String>("unknown")
val permissionState: StateFlow<String> = _permissionState

// 更新状态
_permissionState.value = "granted" // 自动通知所有活跃订阅者

2.3 SharedFlow —— 可配置的事件总线

SharedFlow 是热流,相比 StateFlow 它没有初始值,也不强制只保留最后一个值。通过 replay 参数可以指定新订阅者收到之前发射的多少个值,extraBufferCapacity 控制缓冲区大小以处理背压。

  • 无初始值:适合发送一次性事件(如导航、Toast)。
  • 可自定义重放:replay = 0 则新订阅者不会收到历史事件。
  • 常用场景:事件总线、通知、权限请求回调。
// 创建没有重放、缓冲的事件流
val eventFlow = MutableSharedFlow<PermissionEvent>(replay = 0)

// 发射事件
eventFlow.emit(PermissionEvent.Granted)

// 收集(仅当订阅后发射的事件才会收到)
scope.launch { eventFlow.collect { event -> handleEvent(event) } }

2.4 三者特性对比

特性FlowStateFlowSharedFlow
冷流 / 热流冷热热
是否有初始值无有(必填)无(可空)
是否缓存值不缓存始终缓存最新值可配置 replay
是否去重否是(equals 比较)否
背压处理默认挂起生产者丢旧值(conflated)缓冲 + 挂起策略
订阅者数量每个订阅者独立共享同一实例共享同一实例
常用场景一次性/独立计算流UI 状态(ViewModel)事件 / 通知 / 消息

2.5 选择策略

  1. UI 状态更新 → 使用 StateFlow。它天然支持最新值、去重,与 collectAsState() 配合完美。
  2. 一次性事件(如 Snackbar、导航、权限结果) → 使用 SharedFlow 并设 replay = 0。确保事件只被一个新订阅者消费一次。
  3. 独立计算或网络请求流 → 使用 Flow。每次收集都需要重新触发生产者。
常见误区:不要用 StateFlow 发送一次性事件,因为它会立即重复最新值给新订阅者,导致事件重复处理。反之也不要用 SharedFlow 管理状态,因为它没有初始值且不会自动去重。
调试建议:运行 flowOn(Dispatchers.IO) 改变上游上下文时,务必理解 Flow 的上下文保留规则;StateFlow 和 SharedFlow 的发射应该发生在同一个协程中,避免竞态。

2.6 性能与稳定性

  • 状态流去重:StateFlow 内部使用 distinctUntilChanged 过滤相等值,减少不必要的 UI 刷新。
  • 缓冲区大小:SharedFlow 的 extraBufferCapacity 默认 0,若生产者快于消费者且无缓冲,会导致挂起;根据事件频率适当增大(如 64)。
  • 避免泄漏:所有流都必须在 ViewModel 或可感知生命周期的作用域中收集,使用 viewModelScope 或 Lifecycle.repeatOnLifecycle。