什么是 SharedFlow?Android 中 SharedFlow 与 StateFlow 的区别

作者: IT Sectr 发布日期: 2026-02-20 阅读时间: 9 分钟

SharedFlow 是 Kotlin Coroutines 库中的一种热响应流,针对不应在屏幕旋转或订阅者重建时重复的一次性事件(one-shot events)进行了优化。我们将展示 SharedFlow 与 StateFlow 的区别:与 StateFlow 不同,SharedFlow 不为新订阅者保留最新值,并支持 replay、extraBufferCapacity 和 onBufferOverflow 的配置。根据 Google(Android Developers, 2025)的说法,SharedFlow 是导航命令、Snackbar 消息和其他应仅处理一次的事件的推荐解决方案。

要点

  • SharedFlow — 用于一次性事件的 hot flow:新订阅者不设置 replay 将不会收到先前的值。
  • MutableSharedFlow — 可变的版本,具有用于发送事件的 emit() 和 tryEmit() 方法。
  • SharedFlow vs StateFlow:SharedFlow 不会合并值(可以缓冲多个值),不需要初始值,适用于一次性事件。
  • replay — 向新订阅者重放的最近事件数量(默认为 0)。
  • extraBufferCapacity — 超出 replay 的额外事件缓冲区,防止 emit() 阻塞。

Kotlin 中的 SharedFlow 是什么?

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 vs StateFlow:何时使用什么

SharedFlow 和 StateFlow 之间的选择取决于所传递数据的语义:状态(StateFlow)或事件(SharedFlow)。以下是带有示例的明确标准。

标准SharedFlowStateFlow
语义一次性事件(导航、toast、警报)UI 状态(列表、加载、错误)
初始值不需要必需
订阅时重复仅当 replay > 0 时总是最新值
合并否——事件不会丢失(如果缓冲区未溢出)是——仅保留最后一个
缓冲可通过 replay + extraBufferCapacity 配置仅为 1(replay=1 固定)
使用navigationEvent、showSnackbar、openDialogitems、isLoading、uiState

最简单的规则:如果数据在屏幕旋转时应显示——这是状态(StateFlow)。如果事件在屏幕旋转时不应重复——这是一次性事件(SharedFlow)。例如,「带有错误消息的 toast」——SharedFlow:屏幕旋转时 toast 不应再次显示。「产品列表」——StateFlow:屏幕旋转时列表应保持在屏幕上。

在 IT Sectr,我们将 SharedFlow 用于:导航命令(切换到屏幕、打开深层链接)、UI 事件(Snackbar、AlertDialog)、系统通知(后台数据更新、支付结果)、分析事件(日志记录、跟踪)。

MutableSharedFlow:emit、tryEmit 和缓冲

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 个事件而无丢失。

SharedFlow 用于一次性事件:Event 模式

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 已经释放了它。这保证了导航到订单跟踪屏幕仅发生一次,而不是每次屏幕旋转时都发生。

SharedFlow 参数:replay、extraBufferCapacity、onBufferOverflow

MutableSharedFlow 的构造函数接收三个决定缓冲区行为的参数。错误的配置可能导致事件丢失或 emit() 阻塞。

参数类型默认值描述
replayInt0向新订阅者重放的最近事件数量。0 = 不重放,1 = 如同 StateFlow
extraBufferCapacityInt0超出 replay 的额外缓冲区。事件存储在环形缓冲区中。64——大多数场景的推荐限制
onBufferOverflowBufferOverflowSUSPEND缓冲区满时的策略: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——这保证没有事件会丢失,即使以阻塞发送者为代价。

代码示例:Kotlin 中的 SharedFlow

示例 1:使用 SharedFlow 与 Jetpack Navigation 进行导航

导航命令是 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()
            }
        }
    }
}

示例 2:SharedFlow 与 Room 和 Flow 操作符

复杂场景:使用 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 模式。

常见问题

SharedFlow 会丢失事件吗?

会,如果缓冲区溢出且 onBufferOverflow = DROP_OLDEST 或 DROP_LATEST。SharedFlow 不保证每个事件的送达——它不是消息队列(如 Channel)。如果需要所有事件的保证送达,请使用具有不溢出缓冲区(UNLIMITED)的 Channel 或 BroadcastChannel(已弃用)。对于 UI 事件,丢失过期事件(例如旧的导航)是预期行为,而非错误。

SharedFlow 与 Channel 有何不同?

Channel 是一个 FIFO 队列,其中每个事件仅送达一个订阅者(点对点)。SharedFlow 是广播:每个事件送达所有活跃的订阅者。SharedFlow 更接近 BroadcastChannel(已弃用),适用于「一对多」场景。Channel 用于「一对一」(线程池、pipeline)。根据 JetBrains 的建议,SharedFlow 是所有新项目中 BroadcastChannel 的替代品。

如何使 SharedFlow 线程安全?

SharedFlow 已经是线程安全的——emit() 和 collect() 已正确同步。多个线程可以无锁地调用 emit(),所有活跃订阅者将以正确的顺序接收事件。tryEmit() 是非阻塞的——如果缓冲区已满则返回 false。对于高负载系统,使用带 DROP_OLDEST 的 tryEmit()——这可以防止线程阻塞。

为什么 SharedFlow 不用于状态?

没有 replay=1 的 SharedFlow 不保留最新值——屏幕旋转时新订阅者不会收到当前状态,UI 将保持空白。当 replay=1 时,SharedFlow 的行为类似于 StateFlow,但失去了通过 equals() 进行比较的优化,这会导致在重复发送相同值时产生不必要的通知。StateFlow 是状态的正确选择;SharedFlow 用于事件。

如何测试 SharedFlow?

要测试 SharedFlow,请使用 Turbine——用于测试 Flow 的 Kotlin 库。Turbine 允许您单独检查每个发射,带有超时和完成检查。示例:viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }。也可以在 runTest 中使用 .toList() 并指定预期事件的数量。

总结

  • SharedFlow——用于一次性事件的热响应流,不绑定于最新状态。
  • SharedFlow vs StateFlow:SharedFlow——事件(导航、toast、警报),StateFlow——状态(列表、加载、错误)。
  • MutableSharedFlow 使用 replay=0、extraBufferCapacity=5、DROP_OLDEST——UI 事件的标准配置。
  • emit()——用于阻塞式发送的 suspend 函数;tryEmit()——返回 Boolean 结果的非 suspend 函数。
  • UiEvent 与 sealed class 模式——将一次性事件从 ViewModel 传递到 View 的推荐方式。
  • SharedFlow 保证送达每个订阅者,但在缓冲区溢出时不保证每个事件的送达
  • 在一个 ViewModel 中组合 SharedFlow + StateFlow——分离状态和事件架构的最佳模式。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读