StateFlow 和 SharedFlow 都是 Kotlin 协程中的数据流类型,属于热数据流(Hot Flow)。官方概念简介如下:
StateFlow:一个状态容器式可观察数据流,可以向其收集器发出当前状态和新状态。它始终包含一个值,适合表示 UI 状态。
SharedFlow:SharedFlow 是 StateFlow 的可配置性极高的泛化数据流(StateFlow 继承于 SharedFlow),适合事件通知或需要重放历史值的场景。
对于两者的基本使用以及区别,本文会给出一些关于如何在业务中选择合适热流的建议,以及详细的单元测试代码实现。
StateFlow 的一般用法
以读取数据库数据为例,Repository 负责从数据库读取相应数据并返回一个 flow,在 ViewModel 收集这个 flow 中的数据并更新状态(StateFlow)。在 MVVM 模型中,ViewModel 中暴露出来的 StateFlow 应该是 UI 层中唯一的可信数据来源,注意是唯一,这点跟使用 LiveData 的时候不同。
为什么要在 ViewModel 中暴露热流而不是冷流?
如果我们如果暴露出的是普通的冷流(Cold Flow),会导致每次有新的流收集者时就会触发一次 emit,造成资源浪费。例如,当用户切换 Tab 导致 Activity 重建时,冷流会重新执行查询逻辑,可能导致重复请求网络或数据库。
所以如果 Repository 提供的只有简单的冷流怎么办?很简单,将之转换成热流就好了!通常可以采用以下两种方式:
- 手动转换:还是正常收集冷流,收集到一个数据就往另外构建的 MutableStateFlow 或 MutableSharedFlow 发送。
- 使用拓展函数:使用
stateIn或shareIn拓展函数转换成热流。
既然官方给我们提供了拓展函数,那肯定是直接使用这个方案最好,使用方式如下:
private const val DEFAULT_TIMEOUT = 500L
@HiltViewModel
class MyViewModel @Inject constructor(
userRepository: UserRepository
): ViewModel() {
val userFlow: StateFlow<UiState> = userRepository
.getUsers()
.asResult() // 此处返回 Flow<Result<User>>
.map { result ->
when(result) {
is Result.Loading -> UiState.Loading
is Result.Success -> UiState.Success(result.data)
is Result.Error -> UiState.Error(result.exception)
}
}
.stateIn(
scope = viewModelScope,
initialValue = UiState.Loading,
started = SharingStarted.WhileSubscribed(DEFAULT_TIMEOUT)
)
}
其中 started 参数保证了当配置改变(如屏幕旋转)时不会重新触发订阅,避免不必要的计算。DEFAULT_TIMEOUT 定义了停止订阅后多久清除缓存的状态。
在一些业务复杂的页面,比如首页,通常会有多个数据来源,也就有多个 flow,为了保证单一可靠数据源原则,我们可以使用 函数将多个 flow 组成一个 flow,然后再使用 函数转换成 StateFlow。

