SharedFlow 是 Kotlin Coroutines 库中的一种热响应流,针对不应在屏幕旋转或订阅者重建时重复的一次性事件(one-shot events)进行了优化。我们将展示 SharedFlow 与 StateFlow 的区别:与 StateFlow 不同,SharedFlow 不为新订阅者保留最新值,并支持 replay、extraBufferCapacity 和 onBufferOverflow 的配置。根据 Google(Android Developers, 2025)的说法,SharedFlow 是导航命令、Snackbar 消息和其他应仅处理一次的事件的推荐解决方案。
要点
SharedFlow 是来自 kotlinx.coroutines.flow 库的一种热流(hot flow),与 StateFlow 不同,它不绑定于单一状态,可以向任意订阅者发送任意数量的事件。SharedFlow 是 StateFlow 的基础类型——StateFlow 正是通过 SharedFlow 以 replay = 1 实现的。
SharedFlow 的关键特性是它不必保留最新值。默认情况下(replay = 0),新订阅者在发送新事件之前不会收到任何内容。这使得 SharedFlow 非常适合事件应仅处理一次的场景:导航、Snackbar、系统通知、QR 码扫描结果。
SharedFlow 在 kotlinx.coroutines 1.4.0(2020 年 11 月)中与 StateFlow 一同被稳定化。根据 Kotlin Coroutines 文档(2025),SharedFlow 使用细粒度锁来同步订阅者,并提供高达 1000+ 并发订阅者的线性可扩展性而性能不降级,这已由 JetBrains 的测试证实。
SharedFlow 和 StateFlow 之间的选择取决于所传递数据的语义:状态(StateFlow)或事件(SharedFlow)。以下是带有示例的明确标准。
| 标准 | SharedFlow | StateFlow |
|---|---|---|
| 语义 | 一次性事件(导航、toast、警报) | UI 状态(列表、加载、错误) |
| 初始值 | 不需要 | 必需 |
| 订阅时重复 | 仅当 replay > 0 时 | 总是最新值 |
| 合并 | 否——事件不会丢失(如果缓冲区未溢出) | 是——仅保留最后一个 |
| 缓冲 | 可通过 replay + extraBufferCapacity 配置 | 仅为 1(replay=1 固定) |
| 使用 | navigationEvent、showSnackbar、openDialog | items、isLoading、uiState |
最简单的规则:如果数据在屏幕旋转时应显示——这是状态(StateFlow)。如果事件在屏幕旋转时不应重复——这是一次性事件(SharedFlow)。例如,「带有错误消息的 toast」——SharedFlow:屏幕旋转时 toast 不应再次显示。「产品列表」——StateFlow:屏幕旋转时列表应保持在屏幕上。
在 IT Sectr,我们将 SharedFlow 用于:导航命令(切换到屏幕、打开深层链接)、UI 事件(Snackbar、AlertDialog)、系统通知(后台数据更新、支付结果)、分析事件(日志记录、跟踪)。
MutableSharedFlow 是 SharedFlow 的可变版本,具有用于发送事件的 emit()(suspend)和 tryEmit()(非 suspend)方法。如果缓冲区已满且 onBufferOverflow = SUSPEND,emit() 会暂停。tryEmit() 返回 Boolean——事件是否成功添加到缓冲区。
class EventBus {
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<UiEvent> get() = _events
suspend fun sendEvent(event: UiEvent) {
_events.emit(event)
}
fun trySendEvent(event: UiEvent): Boolean {
return _events.tryEmit(event)
}
}
sealed interface UiEvent {
data class ShowSnackbar(val message: String) : UiEvent
data class NavigateTo(val route: String) : UiEvent
data class ShowDialog(val title: String, val message: String) : UiEvent
}
构造函数参数至关重要:replay = 0 保证事件不会为新订阅者重复;extraBufferCapacity = 10——在 UI 订阅之前快速发送事件时的缓冲区;DROP_OLDEST——溢出时的策略:旧事件被丢弃,新事件被保留。根据 Kotlin Coroutines 性能数据(JetBrains, 2024),extraBufferCapacity = 64 的 SharedFlow 每秒处理超过 100,000 个事件而无丢失。
Event 模式(或 UiEvent)——Google 推荐的将一次性事件从 ViewModel 传递到 View 的方式。与状态(StateFlow)不同,事件应仅处理一次,并且在屏幕旋转时不应重复。replay = 0 的 SharedFlow 非常适合此任务。
class CheckoutViewModel : ViewModel() {
private val _uiState = MutableStateFlow<CheckoutState>(CheckoutState.Idle)
val uiState: StateFlow<CheckoutState> get() = _uiState
private val _event = MutableSharedFlow<CheckoutEvent>()
val event: SharedFlow<CheckoutEvent> get() = _event
fun placeOrder() {
viewModelScope.launch {
_uiState.value = CheckoutState.Loading
try {
val orderId = orderRepository.createOrder(cart)
_uiState.value = CheckoutState.Success(orderId)
_event.emit(CheckoutEvent.NavigateToOrderTracking(orderId))
} catch (e: Exception) {
_uiState.value = CheckoutState.Error(e.message)
_event.emit(CheckoutEvent.ShowErrorSnackbar(e.message ?: "订单错误"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
在 View(Activity/Fragment)中:事件订阅应在 lifecycleScope 中使用 repeatOnLifecycle(STATE.STARTED) 执行。每次进入 STARTED 时,订阅会重新创建,但事件不会重复,因为 replay=0 的 SharedFlow 已经释放了它。这保证了导航到订单跟踪屏幕仅发生一次,而不是每次屏幕旋转时都发生。
MutableSharedFlow 的构造函数接收三个决定缓冲区行为的参数。错误的配置可能导致事件丢失或 emit() 阻塞。
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| replay | Int | 0 | 向新订阅者重放的最近事件数量。0 = 不重放,1 = 如同 StateFlow |
| extraBufferCapacity | Int | 0 | 超出 replay 的额外缓冲区。事件存储在环形缓冲区中。64——大多数场景的推荐限制 |
| onBufferOverflow | BufferOverflow | SUSPEND | 缓冲区满时的策略:SUSPEND、DROP_OLDEST、DROP_LATEST |
// 不同场景的配置:
// 1. 一次性 UI 事件(导航、toast)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. 用于状态同步的 Replay 流(如同 StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. 高频事件发送(分析、日志)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
重要:extraBufferCapacity + replay = 总缓冲区大小。如果 emit() 的调用速度快于订阅者处理事件的速度,缓冲区将填满并触发 onBufferOverflow。对于 UI 事件,DROP_OLDEST 是安全策略:旧事件(已过时的导航)会被丢弃以保留新事件。对于金融交易,请使用 SUSPEND——这保证没有事件会丢失,即使以阻塞发送者为代价。
导航命令是 SharedFlow 的经典用例。Fragment 订阅事件并执行导航。屏幕旋转时,命令不会重复。
// ViewModel
class AuthViewModel : ViewModel() {
private val _navEvent = MutableSharedFlow<NavEvent>()
val navEvent: SharedFlow<NavEvent> get() = _navEvent
fun onLoginSuccess() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.HOME))
}
}
fun onLogout() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.LOGIN))
}
}
}
sealed interface NavEvent {
data class NavigateTo(val route: String) : NavEvent
data class NavigateBack(val popUpTo: String? = null) : NavEvent
}
// 在 Fragment 中:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navEvent.collect { navEvent ->
when (navEvent) {
is NavEvent.NavigateTo -> findNavController().navigate(navEvent.route)
is NavEvent.NavigateBack -> findNavController().popBackStack()
}
}
}
}
复杂场景:使用 SharedFlow 进行后台事件通知,结合 StateFlow 用于 UI。
class NotificationViewModel : ViewModel() {
private val _toastMessage = MutableSharedFlow<String>()
val toastMessage: SharedFlow<String> get() = _toastMessage
private val _notifications = MutableStateFlow<List<Notification>>(emptyList())
val notifications: StateFlow<List<Notification>> get() = _notifications
init {
viewModelScope.launch {
notificationChannel
.consumeAsFlow()
.collect { notification ->
_notifications.value = _notifications.value + notification
_toastMessage.emit("新通知:${notification.title}")
}
}
}
fun dismissNotification(id: String) {
_notifications.value = _notifications.value.filter { it.id != id }
}
fun markAllRead() {
viewModelScope.launch {
_notifications.value = _notifications.value.map { it.copy(isRead = true) }
_toastMessage.emit("所有通知已标记为已读")
}
}
}
在此示例中:StateFlow 存储通知列表(状态——在屏幕旋转时保留),SharedFlow 发送 toast 消息(一次性事件——在屏幕旋转时不会重复)。两种 Flow 类型的组合是 Google 自 2022 年起推荐的 ViewModel 模式。
常见问题
会,如果缓冲区溢出且 onBufferOverflow = DROP_OLDEST 或 DROP_LATEST。SharedFlow 不保证每个事件的送达——它不是消息队列(如 Channel)。如果需要所有事件的保证送达,请使用具有不溢出缓冲区(UNLIMITED)的 Channel 或 BroadcastChannel(已弃用)。对于 UI 事件,丢失过期事件(例如旧的导航)是预期行为,而非错误。
Channel 是一个 FIFO 队列,其中每个事件仅送达一个订阅者(点对点)。SharedFlow 是广播:每个事件送达所有活跃的订阅者。SharedFlow 更接近 BroadcastChannel(已弃用),适用于「一对多」场景。Channel 用于「一对一」(线程池、pipeline)。根据 JetBrains 的建议,SharedFlow 是所有新项目中 BroadcastChannel 的替代品。
SharedFlow 已经是线程安全的——emit() 和 collect() 已正确同步。多个线程可以无锁地调用 emit(),所有活跃订阅者将以正确的顺序接收事件。tryEmit() 是非阻塞的——如果缓冲区已满则返回 false。对于高负载系统,使用带 DROP_OLDEST 的 tryEmit()——这可以防止线程阻塞。
没有 replay=1 的 SharedFlow 不保留最新值——屏幕旋转时新订阅者不会收到当前状态,UI 将保持空白。当 replay=1 时,SharedFlow 的行为类似于 StateFlow,但失去了通过 equals() 进行比较的优化,这会导致在重复发送相同值时产生不必要的通知。StateFlow 是状态的正确选择;SharedFlow 用于事件。
要测试 SharedFlow,请使用 Turbine——用于测试 Flow 的 Kotlin 库。Turbine 允许您单独检查每个发射,带有超时和完成检查。示例:viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }。也可以在 runTest 中使用 .toList() 并指定预期事件的数量。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。