通俗易懂 + Android 实战视角,彻底讲清楚SharedFlow

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

特性StateFlowSharedFlowChannel
用途状态(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)
  • 所有订阅者共享同一个收集协程(通过 shareInMutableSharedFlow 内部实现)
  • 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) } }

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wuwu_q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值