由于缺乏热情,他很自然地把所下的决心都慢慢淡忘了。有一天他没去医院,第二天他没去上课;闲散的滋味使人贪恋,他慢慢就完全不去学习了。——《包法利夫人》
处理异步数据流:StateFlow 与 SharedFlow
这两者都是 Kotlin 协程 Flow API 的一部分,用来处理异步数据流,将数据发射出去,供下游监听。它们的主要区别在于 应用场景不同。
我们知道在 Android 页面实现的过程中,所传输的对象分为 状态 和 控制 两种,状态 标识一个页面当前的 UI 数据,是一个 长期、阶段性 的概念;而 控制 则是 引起可见/不可见变化的事件。状态 是持续的,可以获取到任何时间点所处的状态,反过来,任何时间点一定处于某个状态。而 控制 是瞬时的,获取上一次控制是没有意义的,因为它只存在于发生的那一个瞬间。
这两种概念,就分别对应着 StateFlow
(状态流)与 SharedFlow
(控制流)。
StateFlow 状态流
1. 含义
- 状态容器: 专门用于管理 需要持续观察的 UI 状态(如页面数据、加载状态等)。
- 特性:
- 必选初始值: 必须指定初始状态。
- 仅保留最新值: 始终持有最新状态,新订阅者会立即收到当前值。
- 状态去重: 若新值与旧值相同(通过
equals
判断),则不会触发更新。
2. 应用场景
- UI 状态管理: 例如用户信息、列表数据、加载状态(
Loading/Success/Error
)。 - 需要持久化观察的数据: 如主题模式、语言设置等 全局状态。
- 替代 LiveData: 在纯协程架构中,更推荐使用
StateFlow
(需结合Lifecycle.repeatOnLifecycle
避免内存泄漏)。
3. 代码示例
// ViewModel 中定义
private val _uiState = MutableStateFlow<DataState>(DataState.Loading)
val uiState: StateFlow<DataState> = _uiState
// 更新状态
fun loadData() {
viewModelScope.launch {
_uiState.value = DataState.Loading
try {
val data = repository.fetchData()
_uiState.value = DataState.Success(data)
} catch (e: Exception) {
_uiState.value = DataState.Error(e.message)
}
}
}
注意这里赋值时使用
_uiState.value = DataState.Success(data)
用 等号 进行赋值
SharedFlow 控制流
1. 含义
- 事件分发器: 用于处理 一次性事件 或 广播事件(如错误提示、导航事件)。
- 特性:
- 无初始值: 不需要初始值。
- 灵活配置: 通过参数控制事件的重放(
replay
)和缓存(buffer
)策略。 - 多订阅者支持: 所有订阅者共享同一控制流。
2. 应用场景
- 一次性事件: 如显示
Toast
、Snackbar
、导航到其他页面。 - 高频事件: 如搜索框的 实时输入联想(需防抖处理)。
- 广播通知: 如网络状态变化、权限变更等 全局事件。
3. 代码示例
// ViewModel 中定义
private val _toastEvent = MutableSharedFlow<String>()
val toastEvent: SharedFlow<String> = _toastEvent
// 发射事件
fun showToast(message: String) {
viewModelScope.launch {
_toastEvent.emit(message)
}
}
注意这里发射时使用了
_toastEvent.emit(message)
,而在处理 StateFlow 时则是使用=
,关于两者的区别,将在本文后半部分进行说明。
StateFlow 本质上是 SharedFlow 的封装
- 重放次数 0 -> 1
- 无初始值 -> 具备初始值
- 无去重 -> 去重(通过
equals
判断)
// StateFlow 等效于以下配置的 SharedFlow:
val stateFlow = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
stateFlow.tryEmit(initialValue) // 设置初始值
StateFlow 与 SharedFlow 对比表格
特性 | StateFlow | SharedFlow |
---|---|---|
初始值 | 必须提供 | 不需要 |
值更新策略 | 仅保留最新值(类似 ConflatedChannel) | 可配置缓存和重放策略 |
事件去重 | 自动去重(基于 equals) | 无自动去重 |
适用场景 | 持久化状态管理 | 一次性事件或高频事件 |
选择策略
- 用 StateFlow 的场景:
- 需要管理 UI 状态(如用户信息、页面数据)。
- 希望新订阅者立即获取最新状态。
- 需要避免重复状态更新(如防止界面闪烁)。
- 用 SharedFlow 的场景:
- 处理一次性事件(如 Toast、导航)。
- 需要广播事件给多个订阅者。
- 需要自定义事件缓存(如保留最近 N 个事件)。
MutableStateFlow 的 emit 与 update 函数
这两个函数都是用来更新值,以便下游监听变化,在使用场景上有些微的区别。
1. emit 函数
- 作用: 直接设置一个新的值,覆盖当前值。
- 特点:
- 直接替换当前值,不依赖旧值。
- 是挂起函数(需要协程作用域调用)。
- 等同于直接设置
value
属性(如stateFlow.value = newValue
)。
- 使用场景:
- 当新值与旧值无关时(如直接设置固定值、用户输入等)。
- 在协程作用域中更新状态。
- 示例:
// 直接发射新值
stateFlow.emit(100)
// 或非挂起环境下:
stateFlow.value = 100
2. update 函数
- 作用: 基于当前值进行原子性更新。
- 特点:
- 通过 Lambda 接收当前值,返回新值。
- 保证原子性操作(避免并发修改导致竞态条件)。
- 自动处理值更新,确保每次计算基于最新的状态。
- 使用场景:
- 当新值依赖旧值时(如递增、递减、复合状态修改)。
- 需要线程安全更新的场景(多协程/线程同时修改状态)。
- 示例:
// 原子性递增
stateFlow.update { current -> current + 1 }
emit 与 update 对比表格
对比项 | emit | update |
---|---|---|
依赖旧值 | 不需要旧值,直接设置新值。 | 需要基于旧值计算新值。 |
原子性 | 非原子操作(直接覆盖)。 | 原子操作(确保并发安全)。 |
适用场景 | 简单赋值(如重置状态、用户输入)。 | 依赖旧值的复杂更新(如计数器)。 |
冷流 Cold Flow 与热流 Hot Flow
冷流、热流 是 Kotlin 协程中 Flow 的两种不同类型,冷流 是最常见的,通过 flow{...}
即可以启动,其特点是每次收集时都会启动新的数据生产,即 每个收集者都会从头开始接收数据。
热流(StateFlow
、SharedFlow
都属于热流)的数据生产过程,则是独立于收集过程存在的。如果有多个收集者,则它们共享一个数据流,不会各自触发新的数据生产。
1、冷流 Cold Flow
- 定义: 冷流是 惰性数据流,数据生产在每次被收集(collect)时触发,且每个收集者会获得独立的完整数据序列。
- 类似场景: 像在抖音播放视频,每次打开都从头开始播放完整的一条视频。
- 常见实现: 通过
flow { ... }
构建的普通 Flow。
- 核心特点:
- 按需触发: 没有收集者时,数据生产代码不会执行。
- 独立数据序列: 每个收集者触发独立的数据生产流程。
- 无共享状态: 数据生产是“干净”的,不同收集者之间互不影响。
- 适合一次性操作: 如网络请求、数据库查询等。
- 适用场景:
- 需要每次收集时重新获取数据(如实时搜索输入的关键字)。
- 执行一次性任务(如单次网络请求)。
- 数据生产开销较大,需确保只在需要时执行。
- 注意事项:
- 冷流的重复开销,频繁收集冷流可能导致重复执行耗时操作(如多次网络请求),需结合
cachedIn
或shareIn
优化。
- 冷流的重复开销,频繁收集冷流可能导致重复执行耗时操作(如多次网络请求),需结合
- 示例:
// 冷流:每次 collect 时触发新的数据生产
val coldFlow = flow {
repeat(3) { i ->
delay(100)
emit("冷流值 $i")
}
}
// 收集者 1
coldFlow.collect { println("收集者1: $it") } // 输出 0,1,2
// 收集者 2(再次收集时重新生产数据)
coldFlow.collect { println("收集者2: $it") } // 输出 0,1,2
2、热流
- 定义: 热流是 主动数据流,数据生产与收集者无关,即使没有收集者,数据也会被生产并缓存(取决于配置)。多个收集者共享同一数据源。
- 类似场景: 像斗鱼直播,无论是否有观众,内容都会持续推送。
- 常见实现:
StateFlow
、SharedFlow
,或通过stateIn/shareIn
转换后的 Flow。
- 核心特点:
- 主动生产数据: 数据生产在流创建时启动,与收集者无关。
- 数据共享: 多个收集者共享同一数据源,后续收集者可能收到历史数据(取决于配置)。
- 有状态: 通常用于持续的状态更新或事件广播。
- 适合实时场景: 如 UI 状态管理、事件通知。
- 适用场景:
- 需要多个订阅者共享同一数据源(如全局状态管理)。
- 持续的事件流(如传感器数据、实时消息推送)。
- 需要缓存历史数据供新订阅者使用(如保留最后 N 个事件)。
- 注意事项:
- 热流的生命周期管理,热流的数据生产可能长期存活,需通过协程作用域(如
viewModelScope
)控制其生命周期,避免内存泄漏。
- 热流的生命周期管理,热流的数据生产可能长期存活,需通过协程作用域(如
- 示例:
// 冷流转热流:通过 shareIn 共享数据生产
val hotFlow = coldFlow.shareIn(
scope = CoroutineScope(Dispatchers.Default),
started = SharingStarted.WhileSubscribed(),
replay = 1 // 新订阅者收到最近 1 个历史值
)
// 收集者 1(立即开始接收数据)
hotFlow.collect { println("收集者1: $it") } // 可能输出 0,1,2(取决于生产速度)
delay(150) // 延迟后收集者 2 加入
// 收集者 2(收到最近 1 个历史值 + 后续新值)
hotFlow.collect { println("收集者2: $it") } // 可能输出 1,2
3、冷流、热流对比表格
特性 | 冷流(Cold Flow) | 热流(Hot Flow) |
---|---|---|
数据生产触发时机 | 每次收集时触发新的生产 | 独立于收集者,主动生产 |
数据共享 | 每个收集者获得独立数据序列 | 多个收集者共享同一数据源 |
资源开销 | 每次收集都可能重复触发生产(可能浪费) | 单次生产,多个消费者共享(高效) |
初始数据 | 总是从头开始 | 新收集者可能收到历史数据(可配置) |
典型场景 | 一次性任务、按需请求 | 持续状态更新、事件广播 |
常见实现 | flow { ... } 、asFlow() | StateFlow 、SharedFlow |
4、冷流 vs 热流互相转换
- 冷流 -> 热流
通过 shareIn
或 stateIn
操作符将冷流转换为热流,实现数据共享:
// shareIn:转换为 SharedFlow
val hotSharedFlow = coldFlow.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), // 无订阅者时保留 5 秒
replay = 1
)
// stateIn:转换为 StateFlow
val hotStateFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = "初始值"
)
- 热流 -> 冷流
使用较少,一般直接创建冷流即可。
通过 callbackFlow
或 channelFlow
将热数据源包装为冷流:
fun hotToCold(): Flow<Data> = callbackFlow {
val listener = { data: Data -> trySend(data) }
registerHotSource(listener)
awaitClose { unregisterHotSource(listener) }
}