SharedFlow:Kotlin Flow 中用于在多个观察者之间共享事件流的热流(hot flow),特别适合处理“一次性事件”(如 Toast、导航、错误提示),而不会丢失或重复消费。
它是 StateFlow 的“兄弟”,但用途完全不同。
一、一句话理解 SharedFlow
SharedFlow是一个没有初始值、可配置缓冲区、支持多播(multicast)的热流,专为“事件分发”设计——谁订阅,谁收最新(或历史)事件;没人订阅,事件可丢弃。
类比:
StateFlow→ 当前状态(如“开关是开的”)SharedFlow→ 发生过的动作(如“用户点击了按钮”)
二、为什么需要 SharedFlow?
问题场景:ViewModel 要通知 UI “显示一个 Toast”
// ❌ 错误做法:用 StateFlow
private val _toastMessage = MutableStateFlow<String?>(null)
val toastMessage: StateFlow<String?> = _toastMessage
fun showError() {
_toastMessage.value = "网络错误"
}
问题来了:
- 如果 Compose 还没开始收集,就调用了
showError()→ Toast 丢失! - 如果 Compose 重组多次,会重复收到同一个错误消息 → 弹出多次 Toast!
💡 我们需要的是:事件只被消费一次,且新订阅者不应收到旧事件。
这就是 SharedFlow 的用武之地。
三、SharedFlow 核心特性
| 特性 | 说明 |
|---|---|
| 热流(Hot Flow) | 创建后立即运行,不依赖订阅者 |
| 无初始值 | 不像 StateFlow 必须有默认值 |
| 可配置 Replay | 缓存最近 N 个值,新订阅者可收到 |
| 可配置 Buffer | 控制背压策略(挂起 / 丢弃 / 覆盖) |
| 多播(Multicast) | 所有订阅者共享同一个上游 |
四、创建 SharedFlow:两种方式
方式1️⃣:通过 MutableSharedFlow(最常用)
class EventViewModel : ViewModel() {
private val _events = MutableSharedFlow<Event>(
replay = 0, // 新订阅者不收历史事件
extraBufferCapacity = 64, // 额外缓冲区大小
onBufferOverflow = BufferOverflow.SUSPEND // 缓冲区满时挂起发射者
)
val events: SharedFlow<Event> = _events
fun triggerEvent() {
viewModelScope.launch {
_events.emit(SomeEvent) // emit 是 suspend 函数!
}
}
}
方式2️⃣:通过 shareIn(从冷流转换)
val sharedFlow = someColdFlow
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
replay = 1
)
✅ 一般建议:
- 手动发事件 → 用
MutableSharedFlow- 转换现有 Flow → 用
shareIn
五、关键参数详解
1️⃣ replay: Int
- 新订阅者会立即收到 最近
replay个已发射的值 replay = 0→ 只收未来事件(事件流推荐)replay = 1→ 类似 StateFlow,但无初始值
2️⃣ onBufferOverflow: BufferOverflow
当缓冲区满(订阅者处理太慢),如何处理新事件?
| 策略 | 行为 | 适用场景 |
|---|---|---|
SUSPEND | 挂起 emit() 调用者 | 需要保证不丢事件(如支付结果) |
DROP_OLDEST | 丢弃最旧的事件 | 日志、非关键通知 |
DROP_LATEST | 丢弃最新的事件 | 实时数据(如传感器),保留旧的更安全 |
🔥 事件流推荐:
replay = 0+onBufferOverflow = DROP_OLDEST
避免内存堆积,且新事件更重要。
六、Android 实战:一次性事件处理
Step 1️⃣:定义事件类型
sealed interface UiEvent {
data object ShowLoginDialog : UiEvent
data class ShowToast(val message: String) : UiEvent
data class NavigateTo(val route: String) : UiEvent
}
Step 2️⃣:ViewModel 中暴露 SharedFlow
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
private val _uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val uiEvents = _uiEvents.asSharedFlow() // 暴露只读 SharedFlow
fun onProfileClick() {
if (!isUserLoggedIn()) {
viewModelScope.launch {
_uiEvents.emit(UiEvent.ShowLoginDialog)
}
}
}
fun onNetworkError() {
viewModelScope.launch {
_uiEvents.emit(UiEvent.ShowToast("网络连接失败"))
}
}
}
Step 3️⃣:Compose 中安全消费(只消费一次!)
@Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel()) {
LaunchedEffect(Unit) {
viewModel.uiEvents.collect { event ->
when (event) {
is UiEvent.ShowLoginDialog -> showLoginDialog()
is UiEvent.ShowToast -> showToast(event.message)
is UiEvent.NavigateTo -> navController.navigate(event.route)
}
}
}
}
✅ 关键:用
LaunchedEffect(Unit)确保只有一个收集器,避免重复消费。
七、SharedFlow vs StateFlow vs Channel
| 特性 | StateFlow | SharedFlow | Channel |
|---|---|---|---|
| 用途 | 状态(State) | 事件(Event) | 事件(旧方案) |
| 初始值 | ✅ 必须有 | ❌ 无 | ❌ 无 |
| Replay | 固定为 1 | 可配置 | 通常 0 |
| 背压 | 自动覆盖旧值 | 可配置策略 | 需手动处理 |
| 是否过时 | 否 | 否 | ⚠️ Google 推荐迁移到 SharedFlow |
📌 Google 官方建议:
- 状态 → StateFlow
- 事件 → SharedFlow
- 不要再用 Channel 处理 UI 事件!
八、常见误区 & 最佳实践
❌ 误区1:在多个地方同时 collect 同一个 SharedFlow
// ❌ 错误!两个 LaunchedEffect 会导致事件被消费两次
LaunchedEffect(Unit) { vm.events.collect { ... } }
LaunchedEffect(Unit) { vm.events.collect { ... } }
✅ 正确:一个事件流,一个收集器。如果需要分发,用 broadcast 或内部路由。
❌ 误区2:忘记 replay = 0,导致新屏幕收到旧事件
// ❌ replay = 1 → 新进入的页面也会弹出上一个页面的 Toast!
val _events = MutableSharedFlow<UiEvent>(replay = 1)
✅ 正确:UI 事件一律 replay = 0
✅ 最佳实践1:封装成扩展函数
inline fun <reified T> sharedEventFlow() = MutableSharedFlow<T>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 使用
private val _events = sharedEventFlow<UiEvent>()
✅ 最佳实践2:测试 SharedFlow
@Test
fun `when login clicked, should emit ShowLoginDialog`() = runTest {
val viewModel = MainViewModel()
val events = mutableListOf<UiEvent>()
// 启动收集
backgroundScope.launch {
viewModel.uiEvents.toList(events)
}
viewModel.onProfileClick()
assertEquals(1, events.size)
assertTrue(events[0] is UiEvent.ShowLoginDialog)
}
九、底层原理简析
SharedFlow内部维护一个 环形缓冲区(ring buffer)- 所有订阅者共享同一个收集协程(通过
shareIn或MutableSharedFlow内部实现) emit()会将值写入缓冲区,并通知所有订阅者- 缓冲区大小 =
replay + extraBufferCapacity
🔍 所以它比
Channel更高效,更适合高频率事件。
十、总结一句话
SharedFlow是 Kotlin Flow 为“一次性 UI 事件”量身打造的热流解决方案:它确保事件不被重复消费、可配置背压策略、支持多观察者,是现代 Android 架构中替代 Channel 的标准事件总线。
💡 记住这个模板:
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events = _events.asSharedFlow()
// 发送
viewModelScope.launch { _events.emit(SomeEvent) }
// 收集(只在一个地方!)
LaunchedEffect(Unit) { vm.events.collect { handle(it) } }


被折叠的 条评论
为什么被折叠?



