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 不会发射新值。

我的经验: 在 Activity 或 Fragment 里收集 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 做状态管理。结果每次订阅都拿不到当前状态,界面一片空白。后来才意识到,状态和事件是两码事,工具得选对。

注意: 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 在状态管理中的完整脉络:

Flow 状态管理知识体系 ViewModel 数据流分发 StateFlow 持有最新状态 特性 • 粘性订阅:新订阅者立即获取当前值 • 去重:值相等时不发射 • 适用:UI 状态、列表数据、加载状态 SharedFlow 一次性事件 特性 • 非粘性:新订阅者不获取历史事件 • 可配置 replay 和缓冲区 • 适用:导航、Toast、Snackbar 状态与事件分离 → 架构清晰可维护

避坑指南

最后,分享几个我实际踩过的坑:

  • 别用 StateFlow 做事件:我曾经用 StateFlow 做导航事件,结果屏幕旋转后重复导航了两次。换成 SharedFlow 就解决了。
  • 注意 SharedFlow 的背压:默认缓冲区只有 64。如果你短时间内发射大量事件,可能会丢。建议根据场景调整 extraBufferCapacity
  • StateFlow 的初始值别偷懒:我见过有人用 MutableStateFlow(null),然后到处判空。不如直接给个有意义的默认状态。
  • 事件用 sealed class:这样 when 表达式可以穷举,编译器帮你检查有没有漏掉分支。

好了,关于 StateFlow 和 SharedFlow 的状态管理,今天就聊到这。记住一句话:状态用 StateFlow,事件用 SharedFlow,两者分开,你的代码会感谢你的。


公众号:蓝海资料掘金营,微信deep3321