22、Flow与状态管理:StateFlow的状态管理、SharedFlow的事件管理、UI状态与事件分离
说实话,Flow 系列讲到这里,终于要碰最核心的问题了——状态管理。
很多新手用 Flow 时,容易把状态和事件混为一谈。我早期也踩过这个坑,写出来的代码看着挺 Flow,实际上该有的问题一个没少。比如屏幕旋转后状态丢了,或者事件重复消费了。嗯,今天咱们就把这事彻底理清楚。
StateFlow:UI 状态的忠实守卫
StateFlow 是什么?说白了,它是一个有状态的 Flow。它永远持有最新的值,并且只在值真正变化时才发射。
我个人习惯把 StateFlow 用在 ViewModel 里,作为 UI 状态的容器。为什么?因为它天然支持「粘性」——新订阅者一上来就能拿到当前状态,不用等下一次发射。
- 始终持有最新值
- 值相等时不重复发射(基于 equals)
- 支持粘性订阅
- 线程安全
来看一个实际例子。假设我们做一个用户资料页面:
class UserProfileViewModel(
private val userRepository: UserRepository
) : ViewModel() {
// 私有的 MutableStateFlow,对外暴露不可变的 StateFlow
private val _uiState = MutableStateFlow(UserProfileState())
val uiState: StateFlow<UserProfileState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
try {
val user = userRepository.getUser(userId)
_uiState.value = _uiState.value.copy(
user = user,
isLoading = false
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message,
isLoading = false
)
}
}
}
}
data class UserProfileState(
val isLoading: Boolean = false,
val user: User? = null,
val error: String? = null
)
这里有个细节:我用 copy() 来更新状态。为什么?因为 StateFlow 的相等性检查是基于 equals 的。如果你直接修改 data class 的字段,引用没变,StateFlow 不会发射新值。
repeatOnLifecycle。否则后台时还在收集,容易出问题。
SharedFlow:事件的一锤子买卖
StateFlow 适合状态,那事件呢?比如弹 Toast、导航跳转、显示 Snackbar——这些是一次性的,不需要「粘性」。你想想看,用户旋转屏幕后,不应该再弹出刚才的 Toast 吧?
这时候就该 SharedFlow 登场了。
SharedFlow 和 StateFlow 最大的区别:它不持有最新值,也不做相等性检查。它就是一个纯粹的事件总线,发射了就发射了,新订阅者拿不到历史事件。
class EventViewModel : ViewModel() {
// SharedFlow 配置:replay=0 表示不重放历史事件
private val _events = MutableSharedFlow<UiEvent>(replay = 0)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun performAction() {
viewModelScope.launch {
// 做一些操作...
_events.emit(UiEvent.ShowToast("操作成功"))
}
}
}
sealed class UiEvent {
data class ShowToast(val message: String) : UiEvent()
data class NavigateTo(val route: String) : UiEvent()
object ShowLoading : UiEvent()
}
我曾经在项目里犯过一个错:用 SharedFlow 做状态管理。结果每次订阅都拿不到当前状态,界面一片空白。后来才意识到,状态和事件是两码事,工具得选对。
emit() 是挂起函数。如果你在非协程环境里发射事件,可以用 tryEmit()。但要注意,tryEmit 可能失败(当缓冲区满且没有订阅者消费时)。
UI 状态与事件分离:架构的黄金法则
把状态和事件分开,是我认为 Android 架构里最重要的一条原则。为什么?
- 状态是持久的:屏幕旋转、进程重建后,状态应该恢复
- 事件是一次性的:消费完就没了,不需要恢复
- 测试更简单:状态用断言,事件用监听,职责清晰
来看一个完整的例子,把两者结合起来:
class OrderViewModel : ViewModel() {
// 状态
private val _state = MutableStateFlow(OrderState())
val state: StateFlow<OrderState> = _state.asStateFlow()
// 事件
private val _events = MutableSharedFlow<OrderEvent>(replay = 0)
val events: SharedFlow<OrderEvent> = _events.asSharedFlow()
fun placeOrder() {
viewModelScope.launch {
_state.value = _state.value.copy(isSubmitting = true)
try {
val result = orderRepository.submit(_state.value.order)
_state.value = OrderState() // 重置状态
_events.emit(OrderEvent.OrderSuccess(result.orderId))
} catch (e: Exception) {
_state.value = _state.value.copy(
isSubmitting = false,
error = e.message
)
_events.emit(OrderEvent.ShowError(e.message ?: "未知错误"))
}
}
}
}
data class OrderState(
val order: Order = Order(),
val isSubmitting: Boolean = false,
val error: String? = null
)
sealed class OrderEvent {
data class OrderSuccess(val orderId: String) : OrderEvent()
data class ShowError(val message: String) : OrderEvent()
}
在 UI 层收集时,我习惯这样处理:
@Composable
fun OrderScreen(viewModel: OrderViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
// 处理事件
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is OrderEvent.OrderSuccess -> {
// 导航到订单详情
navController.navigate("order_detail/${event.orderId}")
}
is OrderEvent.ShowError -> {
// 显示 Snackbar
snackbarHostState.showSnackbar(event.message)
}
}
}
}
// 根据状态渲染 UI
OrderContent(
state = state,
onPlaceOrder = { viewModel.placeOrder() }
)
}
你看,状态和事件各司其职。状态驱动 UI 渲染,事件驱动一次性操作。这样代码清晰多了,对吧?
知识体系总览
下面这张图总结了 Flow 在状态管理中的完整脉络:
避坑指南
最后,分享几个我实际踩过的坑:
- 别用 StateFlow 做事件:我曾经用 StateFlow 做导航事件,结果屏幕旋转后重复导航了两次。换成 SharedFlow 就解决了。
- 注意 SharedFlow 的背压:默认缓冲区只有 64。如果你短时间内发射大量事件,可能会丢。建议根据场景调整
extraBufferCapacity。 - StateFlow 的初始值别偷懒:我见过有人用
MutableStateFlow(null),然后到处判空。不如直接给个有意义的默认状态。 - 事件用 sealed class:这样 when 表达式可以穷举,编译器帮你检查有没有漏掉分支。
好了,关于 StateFlow 和 SharedFlow 的状态管理,今天就聊到这。记住一句话:状态用 StateFlow,事件用 SharedFlow,两者分开,你的代码会感谢你的。