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 区别
| 对比项 | StateFlow | SharedFlow |
|---|---|---|
| 设计目的 | 持有并更新单一状态值 | 广播事件流,可支持多个订阅者 |
| 初始值 | 必须提供初始值 | 可无初始值 |
| 重复值过滤 | 默认过滤与上次相同的值(equals) | 默认不过滤,可通过 replay 参数控制重放 |
| 重放策略 | 始终重放当前最新值给新订阅者 | 通过 replay 参数指定重放数量(默认0) |
| 背压处理 | 不缓存额外事件,只保留最新值 | 可通过 extraBufferCapacity 和 onBufferOverflow 配置 |
| 典型用途 | UI 状态持有(如 ViewModel 中的 LiveData 替代品) | 事件总线、一次性事件通知、权限变更广播 |
权限监听场景选型:权限变更需要将事件传递给所有活跃的订阅者,且同一事件不应重复发送,因此选用
SharedFlow(replay = 0)作为核心事件载体;若同时需要保留最新权限状态,可额外使用 StateFlow 作为快照。
4. 冷流与热流完整对比流程图
5. 实现流程:如何选择并切换流类型
- 需求分析:确认数据流是一次性的(网络请求)还是持续性的(状态/事件)。
- 冷流首选:若使用
flow { }或flowOf()即可满足需求,优先使用冷流(天然背压,易于调试)。 - 热流引入时机:需要多个订阅者、事件广播或状态共享时,更换为
MutableStateFlow/MutableSharedFlow。 - 配置参数:
SharedFlow通过replay控制重放历史事件,extraBufferCapacity缓存溢出发射;StateFlow必须提供初始值。 - 测试验证:使用
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 回调实现实时监听。