6、StateFlow与SharedFlow:StateFlow的使用场景、SharedFlow的使用场景、与LiveData的迁移

说到 Flow 家族里的两个重要成员——StateFlow 和 SharedFlow,我其实在项目里跟它们打过不少交道。刚开始接触时也觉得有点绕,但用顺手之后,你会发现它们比 LiveData 灵活太多了。

说白了,StateFlow 和 SharedFlow 都是 Kotlin 协程体系下的热流(Hot Flow)。什么叫热流?就是不管有没有订阅者,数据都在那里,随时可以取。跟冷流(Cold Flow)不一样,冷流是每次订阅才重新执行生产逻辑。

嗯,我们先从 StateFlow 说起。

StateFlow:状态持有者

StateFlow 的核心特点就一个——它始终持有最新的状态值。你任何时候订阅它,都能立刻拿到当前值。这一点跟 LiveData 非常像。

我个人习惯把 StateFlow 用在 ViewModel 里,作为 UI 状态的容器。举个例子:

class MainViewModel : ViewModel() {
    // 定义一个 StateFlow,初始值为 Loading
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val data = repository.fetchData()
                _uiState.value = UiState.Success(data)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "未知错误")
            }
        }
    }
}

你看,StateFlow 通过 .value 直接读写,非常直观。而且它天然支持协程的 collect 操作,在 Fragment 或 Activity 里收集起来也很方便。

核心要点:StateFlow 是粘性的——新订阅者会立即收到当前值。如果你不需要这个行为,可以考虑 SharedFlow。

我在项目中遇到过一个问题:多个 Fragment 共享同一个 ViewModel 时,StateFlow 的粘性导致每次 Fragment 重新创建都会重新触发一次数据加载。后来我用了 replay 参数为 0 的 SharedFlow 才解决。

SharedFlow:事件总线

SharedFlow 比 StateFlow 更通用。它不强制持有最新值,你可以配置重放(replay)数量、缓冲区大小等。说白了,StateFlow 是 SharedFlow 的一个特例——replay = 1 且没有缓冲区溢出策略。

SharedFlow 最适合的场景是——一次性事件。比如导航、Snackbar 提示、弹窗等。这些事件不应该被重放,否则用户会看到重复的提示。

class EventViewModel : ViewModel() {
    // 定义一个 SharedFlow,replay = 0,不重放
    private val _events = MutableSharedFlow<UiEvent>(replay = 0)
    val events: SharedFlow<UiEvent> = _events.asSharedFlow()

    fun triggerEvent() {
        viewModelScope.launch {
            _events.emit(UiEvent.ShowSnackbar("操作成功"))
        }
    }
}

sealed class UiEvent {
    data class ShowSnackbar(val message: String) : UiEvent()
    data class NavigateTo(val route: String) : UiEvent()
}

这里要注意,SharedFlow 的 emit 是挂起函数,如果收集端处理太慢,可能会阻塞发送端。我建议用 tryEmit 做非阻塞发送,或者配置合适的缓冲区策略。

避坑指南:我曾经在项目里用 SharedFlow 做事件总线,结果忘记设置 replay = 0,导致屏幕旋转后事件被重放,用户看到了两次 Toast。嗯,从那以后我每次写 SharedFlow 都会检查 replay 参数。

与 LiveData 的迁移

很多团队从 LiveData 迁移到 StateFlow,原因其实很简单——LiveData 不支持协程,而且只能在 Android 主线程上观察。StateFlow 配合 flowWithLifecycle 可以做到同样的事情,还更灵活。

迁移步骤其实不复杂:

  1. ViewModel 层:MutableLiveData 换成 MutableStateFlow,暴露为 StateFlow
  2. 数据层:LiveData 的转换操作(如 mapswitchMap)换成 Flow 的 mapflatMapLatest
  3. UI 层:observe 换成 collect,配合 repeatOnLifecycleflowWithLifecycle

举个例子,这是 LiveData 的写法:

// LiveData 写法
val userLiveData: LiveData<User> = repository.getUserLiveData()

viewModel.userLiveData.observe(viewLifecycleOwner) { user ->
    // 更新 UI
}

迁移成 StateFlow 后:

// StateFlow 写法
val userStateFlow: StateFlow<User> = repository.getUserFlow()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = User()
    )

// 在 Fragment 中收集
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.userStateFlow.collect { user ->
            // 更新 UI
        }
    }
}

你想想看,StateFlow 的优势在哪里?

  • 协程原生:可以跟其他 Flow 操作符无缝组合
  • 线程安全:不需要 postValue 这种 hack
  • 测试友好:可以用 TestCoroutineDispatcher 控制时间
  • 生命周期感知:配合 repeatOnLifecycle 自动管理
我的建议:迁移时不要一次性改完所有代码。先挑一个简单的页面做试点,跑通流程后再逐步推广。我当初就是这么干的,踩的坑少很多。

不过也要注意,StateFlow 不是 LiveData 的完全替代品。LiveData 在 DataBinding 里用起来更顺手,如果你项目重度依赖 DataBinding,暂时保留 LiveData 也没问题。

最后,我整理了一张对比图,帮你快速理解三者的区别:

LiveData vs StateFlow vs SharedFlow 对比 特性 LiveData StateFlow SharedFlow 粘性(重放) 是(自动) 是(replay=1) 可配置 协程支持 不支持 原生支持 原生支持 线程安全 需 postValue 安全 安全 一次性事件 不适合 不适合 适合 DataBinding 原生支持 需适配 需适配 生命周期感知 自动 需 repeatOnLifecycle 需 repeatOnLifecycle 选择建议:UI状态用 StateFlow,一次性事件用 SharedFlow,遗留项目可保留 LiveData

总结一下我的经验:

  • UI 状态(如加载中、数据、错误):用 StateFlow,粘性行为正好符合需求
  • 一次性事件(如导航、Snackbar):用 SharedFlow,replay = 0
  • 旧项目迁移:从 LiveData 逐步迁移到 StateFlow,不要一次性全改

嗯,StateFlow 和 SharedFlow 其实没那么神秘。你只要记住——StateFlow 是带状态的,SharedFlow 是带事件的。用对场景,代码会清爽很多。

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