Coroutine Flow 基础回顾:冷流与热流、StateFlow 与 SharedFlow 区别、流构建器

在深入实现权限变更监听之前,必须理清 Coroutine Flow 的核心概念。Flow 是 Kotlin 协程中处理异步数据流的统一抽象,根据数据生产与消费的关系分为冷流和热流;而 StateFlow 与 SharedFlow 是热流的两套标准实现。本章将从原理、构建方式、差异场景和调试要点出发,为后续章节打下坚实基础。

1. 冷流与热流:核心原理

特性冷流(Cold Flow)热流(Hot Flow)
定义每次订阅时重新执行生产者逻辑生产者独立于订阅者运行,共享数据
数据产生时机只有存在订阅者时才开始生产即使没有订阅者,生产也可能持续
订阅者关系每个订阅者获得独立数据序列多个订阅者共享同一份数据流
典型构建器flow { } 、flowOf() 、asFlow()MutableStateFlow 、MutableSharedFlow 、channelFlow
背压处理天然支持(挂起发送)需显示配置缓存策略或使用 BufferOverflow
关键认知:冷流适用于一次性的、独立的异步任务(如网络请求、文件读取);热流适用于持续状态维护、事件总线、UI 状态同步等场景。在权限变更监听中,我们需要热流来广播系统权限变化事件。

2. 流构建器详解(API 与示例)

Kotlin 提供了多种构建 Flow 的方式,以下是最常用的三种冷流构建器及一种热流构建器。

2.1 flow { }

最灵活的构建器,在协程上下文中执行挂起代码,通过 emit() 发射数据。

fun countDownToZero(start: Int): Flow<Int> = flow {
    for (i in start downTo 0) {
        delay(500) // 模拟耗时
        emit(i)
    }
}

2.2 flowOf()

用于发射固定元素集合的便捷方式。

val fixedFlow: Flow<String> = flowOf("alpha", "beta", "gamma")

2.3 asFlow()

将集合、序列或数组转换为 Flow。

val listFlow = listOf(1, 2, 3).asFlow()
val sequenceFlow = sequence { yield(10); yield(20) }.asFlow()

2.4 channelFlow(热流预置)

构建一个基于 Channel 的 Flow,允许多个协程并发发射,内部自动处理背压。

fun channelBasedFlow(): Flow<Int> = channelFlow {
    launch {
        send(1)  // 注意:这里用 send() 而非 emit()
    }
    launch {
        send(2)
    }
    awaitClose { /* 清理资源 */ }
}
常见错误:在 flow { } 内部使用 launch 或 withContext 切换调度器时,必须保证 emit() 发生在同一个协程上下文中,否则会抛出 IllegalStateException。推荐使用 channelFlow 或 flowOn 来处理并发。

3. StateFlow 与 SharedFlow 区别

对比项StateFlowSharedFlow
设计目的持有并更新单一状态值广播事件流,可支持多个订阅者
初始值必须提供初始值可无初始值
重复值过滤默认过滤与上次相同的值(equals)默认不过滤,可通过 replay 参数控制重放
重放策略始终重放当前最新值给新订阅者通过 replay 参数指定重放数量(默认0)
背压处理不缓存额外事件,只保留最新值可通过 extraBufferCapacity 和 onBufferOverflow 配置
典型用途UI 状态持有(如 ViewModel 中的 LiveData 替代品)事件总线、一次性事件通知、权限变更广播
权限监听场景选型:权限变更需要将事件传递给所有活跃的订阅者,且同一事件不应重复发送,因此选用 SharedFlow(replay = 0)作为核心事件载体;若同时需要保留最新权限状态,可额外使用 StateFlow 作为快照。

4. 冷流与热流完整对比流程图

冷流 vs 热流 数据流向对比 冷流(Cold Flow) 热流(Hot Flow) 数据源 flow { } (冷流生产者) 订阅者 1 订阅者 2 数据源 SharedFlow (热流生产者) 订阅者 A 订阅者 B 💡 每个订阅者获得独立数据流, 生产者核心逻辑重新执行一次。 💡 所有订阅者共享同一数据流, 生产者逻辑只运行一次。

5. 实现流程:如何选择并切换流类型

  1. 需求分析:确认数据流是一次性的(网络请求)还是持续性的(状态/事件)。
  2. 冷流首选:若使用 flow { } 或 flowOf() 即可满足需求,优先使用冷流(天然背压,易于调试)。
  3. 热流引入时机:需要多个订阅者、事件广播或状态共享时,更换为 MutableStateFlow / MutableSharedFlow。
  4. 配置参数:SharedFlow 通过 replay 控制重放历史事件,extraBufferCapacity 缓存溢出发射;StateFlow 必须提供初始值。
  5. 测试验证:使用 flow.take() 或 flow.test()(来自 kotlinx-coroutines-test)验证数据序列。
// 从冷流转热流示例 —— 将挂起函数返回的 Flow 转为 SharedFlow
fun observePermissions(): Flow<Set<String>> = callbackFlow {
    val listener = { permissions: Set<String> -> trySend(permissions) }
    registerListener(listener)
    awaitClose { unregisterListener(listener) }
}.shareIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5_000),
    replay = 0
)
调试技巧:在开发阶段,使用 flow.onEach { println("emit: $it") } 或 flow.debug() 自定义扩展方法打印发射数据。对于热流,注意订阅者是在协程启动后才挂载,过早订阅可能漏掉已发射的数据(除非设置了 replay > 0)。

6. 常见错误与性能优化

错误现象根因解决方案
冷流中使用 launch 发射数据导致崩溃emit() 必须在流构建器协程内调用改用 channelFlow 或使用 flowOn 切换上下文
热流订阅者收到重复数据SharedFlow 未设置 replay 或缓存溢出重发设置 replay=0,并检查 onBufferOverflow 策略
StateFlow 收集不到初始值订阅发生在赋值之前,但 StateFlow 始终持有最新值确认订阅时协程已启动,初始值确保为非空
背压导致内存溢出(OOM)生产者速率远高于消费者,未配置缓存限制对热流设置 extraBufferCapacity 并指定 DROP_OLDEST;对冷流使用 conflate()
性能优化核心:避免在 flow { } 内执行高频率且无意义的 emit();使用 distinctUntilChanged() 过滤重复值;对重度计算使用 flowOn(Dispatchers.Default) 分离线程;热流场景中合理预估订阅者数量,必要时使用 BroadcastChannel(已废弃)替代方案评估。

掌握了冷流/热流的概念、构建方式和差异选择后,第 3 章将直接使用 SharedFlow 构建权限变更事件总线,并结合 Android 系统的 PackageManager 回调实现实时监听。