在现代 Android 开发中,响应式编程已经成为处理 UI 状态和数据流的标准方式。Kotlin 协程库提供的 StateFlow 和 SharedFlow 是 LiveData 的强大替代品,它们更加灵活、功能更丰富,并且与协程生态无缝集成。本文将深入探讨这两种 Flow 的特点、使用场景以及最佳实践。
一、Flow 家族概览
在深入之前,我们先理清 Kotlin 中 Flow 的层次结构:
Flow<T>:冷流(Cold Stream),每次收集都会从头开始发射数据StateFlow<T>:热流(Hot Stream),始终持有一个状态值,新订阅者立即获得当前值SharedFlow<T>:热流(Hot Stream),可配置的回放和缓冲策略,更通用的热流实现
冷流 vs 热流:冷流是"按需生产",只有消费者订阅时才开始发射数据;热流是"持续生产",无论是否有消费者,数据都会持续产生。
二、StateFlow:状态管理的利器
2.1 什么是 StateFlow?
StateFlow 是一个带有状态的可观察数据流,它始终持有一个最新的状态值,也不能为空。任何新的收集器(Collector)都会立即收到当前的最新值。
kotlin
// 创建 StateFlow
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// 更新状态
_uiState.value = UiState.Success(data)
2.2 核心特性
| 特性 | 说明 |
|---|---|
| 必须有初始值 | 创建时必须提供默认值,不会出现 null 状态的歧义 |
| 仅保留最新值 | 只缓存一个值,新值会覆盖旧值 |
| 去重机制 | 相同值(也就是状态不变)不会触发重复通知 |
| 支持状态读取 | 可以随时通过 .value 同步读取当前状态 |
2.3 与 LiveData 的对比
kotlin
// LiveData 写法
class MyViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count: LiveData<Int> = _count
fun increment() {
_count.value = _count.value!! + 1
}
}
// StateFlow 等价写法
class MyViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.update { it + 1 } // 线程安全的原子更新,内部使用了CAS+失败重试操作
}
}
StateFlow 的优势:
- ✅ 天然支持协程,无需
viewModelScope转换 - ✅ 支持丰富的 Flow 操作符(
map、filter、combine等) - ✅ 默认支持防抖(值不变不通知)
- ✅ 无生命周期依赖,可在任意层使用
2.4 在 UI 层收集 StateFlow
kotlin
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
}
}
}
2.5 状态更新的最佳实践
kotlin
// ❌ 错误:直接修改状态对象(如果 UiState 是 data class)
_uiState.value.name = "New Name"
// ✅ 正确:创建新的状态对象
data class UiState(val name: String, val age: Int)
_uiState.value = _uiState.value.copy(name = "New Name")
// ✅ 更优:使用 update 方法(线程安全)
_uiState.update { currentState ->
currentState.copy(name = "New Name")
}
三、SharedFlow:通用事件总线
3.1 什么是 SharedFlow?
SharedFlow 是一种不带状态的数据流,可以没有初始值,支持配置回放(replay)和缓冲(buffer)策略,非常适合处理一次性事件(如 Toast、导航、Snackbar)。
kotlin
// 创建 SharedFlow
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
// 发送事件
_events.emit(UiEvent.ShowToast("操作成功"))
3.2 核心配置参数
kotlin
val sharedFlow = MutableSharedFlow<Event>(
replay = 1, // 新订阅者收到的历史事件数量
extraBufferCapacity = 2, // 超出 replay 的额外缓冲,给现有订阅者使用
onBufferOverflow = BufferOverflow.DROP_OLDEST // 缓冲溢出策略
)
| 参数 | 说明 | 默认值 |
|---|---|---|
replay |
新订阅者能收到的最近 N 个值 | 0 |
extraBufferCapacity |
超出 replay 的额外缓冲 | 0 |
onBufferOverflow |
缓冲区满时的处理策略 | BufferOverflow.SUSPEND |
溢出策略:
SUSPEND:发送方挂起等待(默认,需确保有消费者)DROP_OLDEST:丢弃最旧的数据DROP_LATEST:丢弃最新的数据
注:buffer 和溢出策略只在有收集器时生效。没有收集器时,emit 不会挂起:有 replay 时更新 replayCache,无 replay 时直接丢弃。
3.3 SharedFlow 的典型应用场景
场景一:一次性事件(Event)
kotlin
sealed class UiEvent {
data class NavigateTo(val route: String) : UiEvent()
data class ShowSnackbar(val message: String) : UiEvent()
data class ShowToast(val message: String) : UiEvent()
}
class NewsViewModel : ViewModel() {
private val _uiEvent = MutableSharedFlow<UiEvent>()
val uiEvent = _uiEvent.asSharedFlow()
fun onNewsClick(newsId: String) {
viewModelScope.launch {
_uiEvent.emit(UiEvent.NavigateTo("detail/$newsId"))
}
}
}
场景二:带缓冲的事件流
kotlin
class SensorViewModel : ViewModel() {
// 保留最近 5 个传感器读数
private val _sensorData = MutableSharedFlow<SensorReading>(
replay = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val sensorData = _sensorData.asSharedFlow()
}
3.4 对比:为什么不用 StateFlow 处理事件?
kotlin
// ❌ 错误:用 StateFlow 处理 Toast 事件
private val _toastEvent = MutableStateFlow<String?>(null)
// 问题1:需要手动清空状态
// 问题2:屏幕旋转后事件会重新触发
// 问题3:去重机制可能导致连续相同事件丢失
// ✅ 正确:用 SharedFlow 处理一次性事件
private val _toastEvent = MutableSharedFlow<String>()
四、StateFlow vs SharedFlow:如何选择?
| 维度 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须有 | 可选(默认为空) |
| 当前状态读取 | 支持 .value |
不支持直接读取 |
| 去重 | 自动去重(equals) | 不去重,每个值都发射 |
| 适用场景 | UI 状态(State) | 一次性事件(Event) |
| 生命周期 | 长期持有 | 瞬时消费 |
| 典型例子 | 加载状态、列表数据、用户信息 | Toast、导航、Snackbar、日志 |
4.1 决策流程图
需要表示状态吗?
├── 是 → 需要读取当前值?
│ ├── 是 → StateFlow
│ └── 否 → SharedFlow (replay=1)
└── 否 → 一次性事件?
├── 是 → SharedFlow (replay=0)
└── 否 → 普通 Flow
五、高级技巧与最佳实践
5.1 结合使用 StateFlow + SharedFlow
在 MVVM 架构中,推荐同时使用两者:
kotlin
class ProfileViewModel : ViewModel() {
// StateFlow:UI 状态
private val _uiState = MutableStateFlow(ProfileUiState())
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
// SharedFlow:一次性事件
private val _events = MutableSharedFlow<ProfileEvent>()
val events: SharedFlow<ProfileEvent> = _events.asSharedFlow()
fun loadProfile(userId: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val profile = repository.getProfile(userId)
_uiState.update { it.copy(isLoading = false, profile = profile) }
} catch (e: Exception) {
_uiState.update { it.copy(isLoading = false) }
_events.emit(ProfileEvent.ShowError(e.message ?: "未知错误"))
}
}
}
}
5.2 使用 combine 合并多个 StateFlow
kotlin
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
private val _sortOrder = MutableStateFlow(SortOrder.RELEVANCE)
// 自动响应任一状态变化
val searchResults: StateFlow<List<Item>> = combine(
_query,
_sortOrder,
::Pair
).flatMapLatest { (query, sort) ->
repository.search(query, sort)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
5.3 stateIn 操作符:冷流转热流
kotlin
// 将普通 Flow 转换为 StateFlow
val stateFlow = repository.getDataFlow()
.map { data -> data.toUiModel() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UiModel.Loading
)
SharingStarted 策略:
Eagerly:立即开始,无论是否有订阅者Lazily:第一个订阅者出现时开始,永久保持活跃WhileSubscribed(timeout):有订阅者时活跃,无订阅者后延迟 timeout 停止(最推荐)
5.4 在 Compose 中使用
kotlin
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// 处理一次性事件
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is ProfileEvent.ShowError -> {
snackbarHostState.showSnackbar(event.message)
}
}
}
}
// 渲染 UI
when {
uiState.isLoading -> LoadingScreen()
uiState.profile != null -> ProfileContent(uiState.profile!!)
}
}
六、常见陷阱与解决方案
陷阱 1:StateFlow 的值比较陷阱
kotlin
// 如果 UiState 没有正确实现 equals,可能导致不必要的重组
data class UiState(val items: List<Item>) // ✅ data class 自动实现 equals
class UiState(val items: List<Item>) // ❌ 类默认 equals 是引用比较
陷阱 2:SharedFlow 事件丢失
kotlin
// ❌ 没有 replay,如果 UI 未准备好,事件会丢失
private val _events = MutableSharedFlow<Event>()
// ✅ 根据场景配置 replay(如果需要新订阅者收到最新事件)
private val _events = MutableSharedFlow<Event>(replay = 1)
陷阱 3:内存泄漏
kotlin
// ❌ 在 Activity/Fragment 中直接 launch
lifecycleScope.launch {
viewModel.state.collect { } // 可能在后台继续运行
}
// ✅ 使用 repeatOnLifecycle
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { }
}
}
七、总结
| StateFlow | SharedFlow | |
|---|---|---|
| 本质 | 状态容器 | 事件广播 |
| 记忆 | 始终记住最新值 | 按配置回放 |
| 读取 | 支持同步读取 .value |
仅支持异步收集 |
| 去重 | 自动去重 | 不去重 |
| 最佳场景 | UI 状态管理 | 一次性事件、日志流 |
掌握 StateFlow 和 SharedFlow 的区别与适用场景,是构建响应式、可维护 Android 应用的关键。记住这个核心原则:
StateFlow 用于状态(State),SharedFlow 用于事件(Event)。
在 MVVM 架构中,将两者结合使用,可以优雅地处理 UI 的所有数据流场景,写出更加清晰、健壮的代码。