前言
元婴大圆满,你已驯服 Flow 的数据洪流。冷流的懒加载、背压的取舍之道,你皆了然于胸。你能用 Flow 构建精密的管道,数据从源头到终端,流转自如。
然而,一个尴尬的场景正在你的代码中潜伏:
商品详情页,你用
flow {}发起网络请求,并在Compose UI中collectAsState()收集。一切正常。但当屏幕旋转时,Activity重建,Flow被重新收集,网络请求再次发起。用户眼睁睁看着Loading闪烁两次,流量被白白浪费。
你猛然想起 Flow 是冷流——每次 collect 都触发一次全新的数据生产。你需要的是一种热流——数据生产独立于收集者,多个收集者共享同一份数据源,屏幕旋转后能直接拿到最新的缓存值。
这正是 Kotlin 协程为你准备的两种热流武器:
StateFlow:一个有状态的、可观察的数据持有者。它始终持有一个当前值,新收集者立即收到当前值。它是LiveData的协程版替代品,专为UI状态管理而生。
SharedFlow:一个可配置缓存和重放策略的、多播的事件流。它不持有“当前值”,但可以缓存历史事件,新收集者可选是否接收历史。它是EventBus的协程版替代品,专为事件分发而生。
本讲是化神境的初阶修炼。你将:
- 彻底搞懂冷流与热流的本质差异,以及何时该用哪种。
- 掌握
MutableStateFlow的用法,理解其“状态”语义。 - 学会用
stateIn将冷流转换为热流,避免重复请求。 - 初探
MutableSharedFlow,理解其与Channel的区别。
准备好点燃热流之火了吗?我们开始。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
冷流 vs 热流:一个决定生死的关键差异
在深入 API 之前,我们必须先建立对“冷”与“热”的精准直觉。
| 对比维度 | 冷流(Flow) | 热流(StateFlow / SharedFlow) |
|---|---|---|
| 执行时机 | 每次 collect 时重新触发生产 | 生产独立于收集者,持续运行 |
| 数据共享 | 不同收集者收到独立的数据流 | 所有收集者共享同一份数据源 |
| 历史值 | 无历史,只接收收集之后发射的值 | StateFlow 始终有当前值;SharedFlow 可配置重放数量 |
| 资源消耗 | 不收集时不消耗资源 | 即使无收集者,也可能消耗资源 |
| 典型场景 | 网络请求、数据库查询 | UI 状态、事件总线、传感器数据 |
flowchart LR
subgraph Cold[冷流 每次独立]
direction TB
C1[收集者1] --> CF1[触发生产1]
C2[收集者2] --> CF2[触发生产2]
CF1 --> R1[结果A]
CF2 --> R2[结果A']
end
subgraph Hot[热流 共享生产]
direction TB
HP[持续生产] --> HS[共享数据源]
HS --> H1[收集者1]
HS --> H2[收集者2]
H1 --> SR1[相同结果]
H2 --> SR2[相同结果]
end
style Cold fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style Hot fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px
style C1 fill:#90caf9
style C2 fill:#90caf9
style HP fill:#ef9a9a
style HS fill:#ffb74d
- 冷流像视频点播:每个用户点击播放,服务器都从头发送一条独立的视频流。
- 热流像电视直播:电视台持续发射信号,无论多少台电视打开,接收的都是同一画面。
在 Android 中,屏幕旋转导致 UI 重建,你通常希望数据不要重新加载,而是直接展示缓存——这正是热流的用武之地。
StateFlow:有状态的 UI 数据持有者
什么是 StateFlow?
在官方定义中:
StateFlow是一个有状态的、可观察的、只读的数据流。它始终持有一个当前值(value属性),并会在值变化时通知所有收集者。新收集者会立即收到当前值。
它本质上是一个带有 Flow 能力的 LiveData,但摆脱了 Android 平台的依赖,且更深度集成协程。
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
// 私有可变版本,用于内部更新
private val _uiState = MutableStateFlow(UiState.Loading)
// 公开只读版本,暴露给 UI
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun updateState(newState: UiState) {
_uiState.value = newState // 更新值,自动通知所有收集者
}
sequenceDiagram
participant VM as ViewModel
participant SF as MutableStateFlow
participant UI1 as UI 收集者1
participant UI2 as UI 收集者2(后加入)
VM->>SF: value = Loading
UI1->>SF: collect
SF-->>UI1: 立即收到 Loading
VM->>SF: value = Success
SF-->>UI1: 收到 Success
UI2->>SF: collect(后加入)
SF-->>UI2: 立即收到 Success(当前值)
StateFlow 的核心特性
- 强一致性:
value的更新是原子的,且collect收到的值序列严格单调(不会收到旧值)。 - 去重:如果新值与当前值相等(通过
equals判断),则不会通知收集者。 - 粘性:新收集者总是先收到当前值,再收到后续更新。
val state = MutableStateFlow(0)
state.value = 1
state.value = 1 // 第二次赋相同值,不会触发通知
Android 实战:用 StateFlow 管理 UI 状态
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class UserViewModel(
private val userRepo: UserRepository
) : ViewModel() {
sealed class UiState {
object Loading : UiState()
data class Success(val user: User) : UiState()
data class Error(val message: String) : UiState()
}
// 私有可变,公开只读
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
loadUser()
}
fun loadUser() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = userRepo.getUser()
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
}
}
Compose UI 中使用 collectAsState() 收集:
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> Text("用户名:${uiState.user.name}")
is UiState.Error -> Text("错误:${uiState.message}", color = Color.Red)
}
}
关键优势:屏幕旋转时,collectAsState() 重新订阅,会立即收到 StateFlow 的当前值(缓存的数据),无需重新请求网络。
stateIn:将冷流转化为热流
实际开发中,数据源往往是冷流(如 Room 的 Flow 查询、Retrofit 的 suspend 函数转换的 Flow)。我们不想每次收集都重新查询数据库或请求网络。
stateIn 操作符正是为此而生:将冷流转换为热 StateFlow。
val userFlow: Flow<User> = userRepo.observeUser() // 冷流
val userStateFlow: StateFlow<User?> = userFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
SharingStarted 三种策略
started 参数决定上游冷流何时开始执行、何时停止。
| 策略 | 行为 | 适用场景 |
|---|---|---|
Eagerly | 立即启动,永不停止 | 数据需要全局持续更新(如全局配置) |
Lazily | 第一个收集者出现时启动,永不停止 | 数据初始化后始终存在(如用户信息) |
WhileSubscribed | 至少有一个收集者时运行,无收集者后延迟停止 | Android 推荐,节省资源 |
// 典型用法:最后一个订阅者离开 5 秒后,取消上游冷流
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
timeline
title SharingStarted.WhileSubscribed 生命周期
section 无订阅者
初始状态 : 上游停止
section 第一个订阅者加入
订阅触发 : 上游启动,开始执行
section 最后一个订阅者离开
等待超时 : 上游继续,等待 5 秒
section 5 秒后仍无订阅者
超时关闭 : 上游停止,释放资源
实战:Room 查询转为 StateFlow
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE id = :id")
fun getUserFlow(id: String): Flow<User> // Room 返回冷流
}
class UserViewModel(
private val userDao: UserDao
) : ViewModel() {
val user: StateFlow<User?> = userDao
.getUserFlow("123")
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
}
这样,多个 UI 组件收集同一个 user StateFlow,Room 查询只执行一次,数据变化时自动更新所有 UI。
SharedFlow:事件总线的协程版替代品
什么是 SharedFlow?
SharedFlow是一个可配置缓存与重放策略的、多播的事件流。它不持有“当前值”,但可以缓存历史事件,新收集者可选择接收历史事件。
它适合一次性事件:导航指令、Toast 提示、埋点上报。
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun sendEvent(event: Event) {
_events.emit(event) // 发射事件,所有收集者都会收到
}
配置参数详解
val sharedFlow = MutableSharedFlow<Int>(
replay = 1, // 缓存最近 1 个值,新订阅者立即收到
extraBufferCapacity = 10, // 额外缓冲区大小
onBufferOverflow = BufferOverflow.DROP_OLDEST // 缓冲区满时丢弃最旧
)
| 参数 | 含义 | 推荐值 |
|---|---|---|
replay | 新收集者能收到的历史事件数量 | 0(不重放)或 1(如 Toast 防止丢失) |
extraBufferCapacity | 除 replay 外的缓冲区容量 | 根据并发量设置 |
onBufferOverflow | 缓冲区满时的处理策略 | DROP_OLDEST 或 SUSPEND |
与 Channel 的对比
| 对比项 | SharedFlow | Channel |
|---|---|---|
| 多播 | 支持,多个收集者都收到 | 不支持,每个值只能被一个消费者接收 |
| 缓存/重放 | 支持 | 不支持 |
| 用途 | 事件分发 | 协程间一对一或一对多通信 |
flowchart LR
subgraph SF[SharedFlow 多播]
E1[emit 事件] --> S[SharedFlow]
S --> C1[收集者A]
S --> C2[收集者B]
S --> C3[收集者C]
end
subgraph CH[Channel 单播]
E2[发送] --> CHL[Channel]
CHL --> R[只有一个接收者]
end
style SF fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style CH fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px
实战:用 SharedFlow 发送导航事件
class LoginViewModel : ViewModel() {
sealed class Event {
object NavigateToHome : Event()
data class ShowToast(val message: String) : Event()
}
private val _events = MutableSharedFlow<Event>(extraBufferCapacity = 10)
val events: SharedFlow<Event> = _events.asSharedFlow()
fun login(username: String, password: String) {
viewModelScope.launch {
try {
// 登录逻辑...
_events.emit(Event.NavigateToHome)
} catch (e: Exception) {
_events.emit(Event.ShowToast("登录失败"))
}
}
}
}
// Compose UI 中收集
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is Event.NavigateToHome -> navController.navigate("home")
is Event.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
}
}
}
注意:使用 SharedFlow 处理导航时,需确保 LaunchedEffect 在组件生命周期内,避免事件丢失。也可使用 replay = 1 防止配置变更导致事件丢失。
shareIn:将冷流转为 SharedFlow
与 stateIn 类似,shareIn 将冷流转换为热 SharedFlow。
val coldFlow: Flow<Int> = flow { ... }
val sharedFlow: SharedFlow<Int> = coldFlow
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
replay = 1
)
适用场景对比
| 转换目标 | 操作符 | 是否有当前值 | 典型用途 |
|---|---|---|---|
| StateFlow | stateIn | 是 | UI 状态 |
| SharedFlow | shareIn | 否 | 事件流、传感器数据 |
flowchart LR
subgraph Cold[冷流]
C[Flow]
end
subgraph Hot[热流转换]
direction TB
SI[stateIn] --> SF[StateFlow 状态]
SH[shareIn] --> SHF[SharedFlow 事件]
end
C --> SI
C --> SH
style Cold fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style Hot fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style SI fill:#90caf9
style SH fill:#90caf9
style SF fill:#a5d6a7
style SHF fill:#a5d6a7
常见错误与避坑指南
错误 1:在 ViewModel 中用 StateFlow 存放大对象列表且频繁更新
// ❌ 每次更新都创建新列表,触发所有收集者重组
_state.value = _state.value.copy(items = newList)
优化:使用 update 函数或确保 equals 正确实现。
// ✅ 使用 MutableStateFlow.update
_state.update { it.copy(items = newList) }
错误 2:忘记设置 replay,导致 SharedFlow 事件在配置变更后丢失
val events = MutableSharedFlow<Event>() // replay=0,新订阅者收不到历史
修复:对于重要的一次性事件(如导航),设置 replay = 1。
错误 3:在 stateIn 中使用 Eagerly 导致不必要的资源消耗
.stateIn(viewModelScope, SharingStarted.Eagerly, null) // 即使无 UI 也在运行
推荐:Android 中优先 WhileSubscribed()。
错误 4:在 Compose 中直接 collect 而非 collectAsState
var state by remember { mutableStateOf(initial) }
LaunchedEffect(Unit) {
viewModel.uiState.collect { state = it } // 写法繁琐
}
正确:val state by viewModel.uiState.collectAsState()
最佳实践
- UI 状态用 StateFlow,事件用 SharedFlow:清晰区分“是什么”与“发生了什么”。
- 始终通过
asStateFlow()/asSharedFlow()暴露只读版本:防止外部意外修改。 - 在 ViewModel 中使用
stateIn转换冷流:避免重复请求,管理生命周期。 - SharedFlow 的
replay按需设置:Toast 等建议replay = 0,导航建议replay = 1。 - 使用
WhileSubscribed作为SharingStarted策略:兼顾实时性与资源节省。 - 对于 StateFlow,使用
update函数原子更新:避免竞态。
总结与下回预告
恭喜,你已点燃热流之火,化神境初阶修炼完成!
本讲核心收获:
- 冷流每次收集独立执行,热流共享同一数据源。
StateFlow持有当前值,适合 UI 状态,粘性去重。MutableStateFlow内部可写,asStateFlow()暴露只读。stateIn将冷流转为 StateFlow,SharingStarted.WhileSubscribed是 Android 首选。SharedFlow支持多播、缓存、重放,适合事件分发。shareIn将冷流转为 SharedFlow。
在下一讲 【化神境·中阶】 中,我们将深入学习 SharedFlow 的高级配置:缓冲区策略、重放深度、与 Channel 的互操作。届时你会明白:
SharedFlow的replayCache如何工作?- 何时该用
SharedFlow而非BroadcastChannel? - 如何在多个协程间高效广播事件?
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 化神境 · 初阶 | 1、StateFlow 状态真意2、 stateIn 冷热转换诀3、 SharedFlow 多播术 | 当前进度:30%修为: 300/1000下一突破: [化神境 · 中阶] (需领悟:SharedFlow 高级配置、replayCache 机制、与 Channel 对比) | 冷流是点播,热流是广播。StateFlow有记忆,SharedFlow可重放。选对了,事半功倍。 |
【本讲思考题】
1、表象题:以下代码有什么问题?
val state = MutableStateFlow(0)
state.value = 1
state.value = 1
println(state.value)
2、场景题:你需要在 ViewModel 中监听一个每秒更新的位置 Flow,并在多个 Compose 组件中显示。如何设计以避免重复请求和内存泄漏?写出核心代码。
3、原理题:StateFlow 是如何实现“新值相等时不通知收集者”的?equals 比较在何处执行?请结合源码简述。
道友,化神境的大门已敞开。掌握了热流,你的响应式架构将如虎添翼。化神境·中阶见。
欢迎一键四连(
关注+点赞+收藏+评论)