这周同事跟我吃饭的时候,谈到了我前几天写的文章,对于里面的 LiveData 颇有同感,不过他说虽然用了几年 SharedFlow,但是一直没有搞懂那三个参数到底什么意思。
我跟他说,没事儿,小菜一碟!
那么,今天,我们就好好讲讲 SharedFlow 三个参数的含义,话不多说,正文开始。
SharedFlow 是 Kotlin Coroutines 中非常重要的一个 API,尤其是在 Android 开发里,它经常被用来替代过去 LiveData + EventWrapper 的一次性事件写法。
但是,SharedFlow 并不是一个“开箱即用就一定正确”的事件流。它的行为很大程度上取决于几个关键参数:
MutableSharedFlow<T>(
replay = 0,
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.SUSPEND
)
这三个参数分别决定了:
replay // 新订阅者能收到几个旧值
extraBufferCapacity // 除 replay 之外,额外给发送方多少缓冲
onBufferOverflow // 缓冲区满了以后怎么办
理解这几个参数,是正确使用 SharedFlow 的关键。
粗略介绍 SharedFlow
我相信大部分开发者对这个 SharedFlow 已经非常熟悉了,但我还是要稍微介绍一下。
SharedFlow 是 Kotlin Coroutines 提供的一种热流。
普通 Flow 是冷流,只有被 collect 的时候才会开始执行。而 SharedFlow 更像一个广播器:事件可以被发送出去,也可以被多个 collector 同时接收。
最常见的写法是:
private val _events = MutableSharedFlow<UiEvent>()
val events = _events.asSharedFlow()
然后在 ViewModel 中发送事件:
viewModelScope.launch {
_events.emit(UiEvent.ShowToast("保存成功"))
}
在 UI 层收集事件:
lifecycleScope.launch {
viewModel.events.collect { event ->
handleEvent(event)
}
}
这看起来很简单,不过一旦你开始用 SharedFlow,你会发现你会碰到很多小 Bug。
搞清楚 SharedFlow,你需要问自己这几个问题:
- 这个事件要不要保存?
- 新订阅者要不要收到旧事件?
- 发送太快时要不要缓冲?
- 缓冲区满了以后怎么办?
这些问题,正是 MutableSharedFlow 参数要解决的。
默认行为
如果你这样写:
val flow = MutableSharedFlow<String>()
它等价于:
val flow = MutableSharedFlow<String>(
replay = 0,
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.SUSPEND
)
这意味着它是一个:没有 replay、没有额外 buffer、缓冲区满了就挂起发送方的 SharedFlow。
在这种情况下:
flow.emit("Hello")
如果当前没有 collector,这个值不会被保存下来。因为 replay = 0,后面再来的订阅者也收不到它,相当于这个值凭空消失了。
只有当前有 collector 的时候,这个值才能被正常收到!
所以默认的 MutableSharedFlow() 更像是:有人听的时候,我就同步广播;没人听的时候,这个事件就过去了。
这也是为什么你在写 UI 事件时,不能只知道 SharedFlow 可以发事件,还要知道它到底有没有缓冲、有没有 replay。
replay
replay 表示:新订阅者开始 collect 时,可以收到最近几个已经发送过的值。
比如:
val flow = MutableSharedFlow<Int>(
replay = 2
)
如果已经发送过:
flow.emit(1)
flow.emit(2)
flow.emit(3)
然后一个新的 collector 才开始 collect:
flow.collect {
println(it)
}
它会先收到最近的两个值:
2
3
所以 replay 的作用就是:给新订阅者重放历史值。
那么 replay 适合什么场景呢?
1. 希望新订阅者拿到最近的数据
比如一个新闻刷新流:
val latestNews = MutableSharedFlow<List<News>>(
replay = 1
)
新的页面订阅时,可以马上拿到最近一次新闻列表。
不过,如果你只是想获取“当前状态”,一般更推荐使用 StateFlow。因为 StateFlow 本身就是为状态设计的。
2. 希望事件具有“粘性”
比如:
val loginEvent = MutableSharedFlow<LoginResult>(
replay = 1
)
如果登录结果已经发出,页面旋转之后重新 collect,还能收到最近一次登录结果。
但这也是危险点。
如果你用它发送 Toast、Snackbar、导航这类一次性事件,replay = 1 可能会导致事件重复消费。
这个当,可能很多开发者都上过。
比如:
val events = MutableSharedFlow<UiEvent>(
replay = 1
)
发送:
_events.emit(UiEvent.ShowToast("保存成功"))
页面旋转后重新 collect,可能又收到上一次 Toast 事件,导致 Toast 再弹一次。
所以:
replay = 1
适合“新订阅者需要旧数据”的场景,不适合普通一次性 UI 事件。
选择建议
一般可以这样理解:
replay = 0
表示:不给新订阅者补发旧值。适合以下场景:
- Toast
- Snackbar
- 导航事件
- 点击事件
- 一次性弹窗事件
而:
replay = 1
表示:新订阅者可以收到最近一次值。适合:
- 最近一次结果
- 最近一次配置
- 最近一次广播数据
- 需要“粘性”的事件
如果你发现自己写:
MutableSharedFlow(replay = 1)
只是为了保存页面状态,那最好考虑一下 MutableStateFlow。
关于 replay 的一个小规律:当你想把 SharedFlow 当做事件,replay 给 0,想把 SharedFlow 当状态,replay 给 1
extraBufferCapacity
这个参数又有什么用呢?
extraBufferCapacity 表示:在 replay 缓存之外,额外给 SharedFlow 增加多少缓冲空间。
比如:
val flow = MutableSharedFlow<String>(
replay = 0,
extraBufferCapacity = 1
)
这表示:它不会把旧值重放给新订阅者,但可以在 collector 暂时没来得及处理时,额外缓冲 1 个值。
这点和 replay 最大的区别是:会影响新订阅者能不能收到旧值。
extraBufferCapacity 主要影响发送方会不会被挂起,以及慢订阅者来不及处理时能不能暂存。
也就是说,extraBufferCapacity 的主要作用是处理背压问题!
replay vs extraBufferCapacity
这两个参数都和“缓存”有关,但目的不同。
| 参数 | 作用 | 新订阅者能收到吗 |
|---|---|---|
replay | 决定新订阅者能收到最近几个值 | 能,最多收到 replay 个 |
extraBufferCapacity | 额外缓冲空间,避免发送方马上挂起 | 单独设置时不参与 replay,但与 replay 配合时总缓冲可能参与 |
比如:
MutableSharedFlow<String>(
replay = 1,
extraBufferCapacity = 0
)
含义是:保存最近 1 个值,新订阅者可以收到。
而:
MutableSharedFlow<String>(
replay = 0,
extraBufferCapacity = 1
)
含义是:不给新订阅者补发旧值,但允许发送方多塞 1 个还没被处理的值。
所以对于一次性 UI 事件,很多时候会这样写:
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 1
)
这样做的好处是:避免页面重新订阅后重复消费旧事件,同时允许事件在 collector 暂时没准备好时,有一个小缓冲空间。
extraBufferCapacity = 0
如果:
MutableSharedFlow<String>(
replay = 0,
extraBufferCapacity = 0
)
那么它没有任何额外缓冲。
这时 emit() 可能会挂起,等待订阅者接收。
而 tryEmit() 在无缓冲 SharedFlow 中通常不适合用。
因为 tryEmit() 不是一个挂起函数,不能挂起等待,这个函数会立即执行返回结果。如果没有缓冲空间,它很可能发不出去。
不过这个 tryEmit 有一点比较反直觉。
我们可以通过查看关于 tryEmit 的文档知道:
仅当
onBufferOverflow为SUSPEND(挂起)且存在订阅者正在收集该共享流时,本次调用才会返回false。
若不存在订阅者,则不会使用缓冲区;此时若配置了replay,最新发送的值会直接存入缓存并覆盖其中的旧数据;若未配置 replay,则该值会被直接丢弃。无论哪种情况,tryEmit都会返回true。
注意第一句话的措辞 —— “正在收集”,也就是说,只有在出现背压的情况下,这个函数才会返回 false,因为下游的订阅已经来不及收集这些值了,出现了“无法立刻接收”的情况。
而其他情况,都是 SharedFlow 的正常处理逻辑,都会返回 true。
很多开发者用 tryEmit 发 UI 事件失败的原因可能就在这里。
onBufferOverflow
onBufferOverflow 表示:当缓冲区满了,但又有新值要发送时,SharedFlow 应该怎么办?
它常见有三个取值:
BufferOverflow.SUSPEND
BufferOverflow.DROP_OLDEST
BufferOverflow.DROP_LATEST
默认值是 BufferOverflow.SUSPEND。
1. SUSPEND
MutableSharedFlow<Int>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.SUSPEND
)
SUSPEND 的含义是:如果缓冲区满了,发送方挂起,等 collector 消费。
适合不能丢事件的场景,例如
- 订单状态事件
- 支付流程事件
- 必须按顺序处理的业务事件
它的特点是安全,但可能让发送方(上游)等待。
2. DROP_OLDEST
它的含义是:缓冲区满了以后,丢掉最旧的值,放入最新的值。
适合:只关心最新事件的场景,比如:
- 搜索关键词变化
- 滚动位置变化
- 进度刷新
- 高频传感器数据
这类场景里,旧值过期很快。
比如用户快速输入:
a
an
and
andr
andro
android
如果处理不过来,保留最新的 android 通常比保留最早的 a 更有意义。
3. DROP_LATEST
它的含义是:缓冲区满了以后,丢掉最新来的值,保留已有缓冲。
适合:当前正在处理的东西更重要,新来的可以暂时忽略,比如:
- 正在处理上一个请求时,新请求到来可以暂时丢弃
- 某些日志采样,保留已有记录,新采样可忽略
- 批量处理任务中,当前批次未完成时新任务可延后
不过在 UI 事件中,DROP_LATEST 用得相对少一些。因为很多时候我们更倾向于保留最新值。
在一起
现在,把三个参数放到一起讨论。可以把 MutableSharedFlow 想象成一个事件管道。
MutableSharedFlow<T>(
replay = A,
extraBufferCapacity = B,
onBufferOverflow = C
)
其中:replay = A 决定:新订阅者能不能拿到旧值,能拿几个。
extraBufferCapacity = B 决定:发送方最多可以额外缓冲几个值,而不必马上挂起(如果你设置的是挂起的话)。
onBufferOverflow = C 决定:缓冲区满了以后,是挂起,丢旧的,还是丢新的。
所以,如果单从缓存来看,SharedFlow 总缓冲能力可以粗略理解为:
总缓冲 = replay + extraBufferCapacity
但是这两个空间的语义不同:
replay部分会给新订阅者重放。extraBufferCapacity部分不会给新订阅者重放。
实战场景
1. 一次性 UI 事件
例如:Toast、Snackbar、Navigation 等推荐:
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 1
)
val events = _events.asSharedFlow()
发送:
fun save() {
viewModelScope.launch {
_events.emit(UiEvent.ShowToast("保存成功"))
}
}
收集:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.events.collect { event ->
when (event) {
is UiEvent.ShowToast -> showToast(event.message)
is UiEvent.Navigate -> navigate(event.route)
}
}
}
}
replay = 0 避免页面重建后重复收到旧事件。
extraBufferCapacity = 1 给短暂的生命周期切换留一点缓冲空间。
这个配置适合大多数 ViewModel 到 UI 的一次性事件。
甚至,也可以作为 EventBus(应用内事件总线)的推荐写法,例如:
- 应用内部广播
- 模块间事件通知
- 当前在线的消费者才关心的事件
2. 最近一次结果
推荐:
private val _latestResult = MutableSharedFlow<Result<Data>>(
replay = 1
)
val latestResult = _latestResult.asSharedFlow()
适合:新订阅者需要马上拿到最近一次结果
比如:
- 网络请求结果
- 最近一次同步状态
- 最近一次定位结果
但如果这是 UI 状态,更推荐 MutableStateFlow()。
本篇文章我提到过了很多次的 MutableStateFlow,这个对比,我放到了文章的后部分,各位耐心看完即可。
3. 高频事件流
比如搜索框输入、滑动位置、进度更新。
推荐:
private val _queryChanges = MutableSharedFlow<String>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
含义是:不重放旧值,但允许一定缓冲(这个缓冲的值可以按照业务而定,64 只是一个假设值);如果处理不过来,丢掉旧值,保留新值。
适合:
- 搜索输入
- 滚动事件
- 拖拽事件
- 实时进度
这种场景一般不要求每一个中间值都被处理,但新值通常比旧值更重要。
4. 不能丢的业务事件
推荐:
private val _events = MutableSharedFlow<BusinessEvent>(
replay = 0,
extraBufferCapacity = 16,
onBufferOverflow = BufferOverflow.SUSPEND
)
含义是:可以缓冲一部分事件,但满了以后不要丢,发送方等待。适合:
- 订单事件
- 支付事件
- 关键业务状态变化
- 需要按顺序处理的任务
SharedFlow 和 StateFlow
很多人容易把 SharedFlow 和 StateFlow 混在一起。
可以用一句话区分:
StateFlow表示“现在是什么状态”。
SharedFlow表示“发生了什么事件”。
比如页面状态:
data class UiState(
val loading: Boolean = false,
val data: List<Item> = emptyList(),
val error: String? = null
)
更适合:
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
而 Toast、Snackbar、导航:
sealed interface UiEvent {
data class ShowToast(val message: String) : UiEvent
data class Navigate(val route: String) : UiEvent
}
更适合:
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 1
)
val events = _events.asSharedFlow()
判断标准很简单:
如果你问的是:页面现在应该长什么样?用 StateFlow。
如果你问的是:刚刚发生了什么动作?用 SharedFlow。
一个小规律:要默认值!StateFlow;不要默认值!SharedFlow。
速查表
| 场景 | replay | extraBufferCapacity | onBufferOverflow |
|---|---|---|---|
| Toast / Snackbar / 导航 | 0 | 1 | SUSPEND |
| 普通事件总线 | 0 | 0 | SUSPEND |
| 新订阅者需要最近一次数据 | 1 | 0 | SUSPEND |
| 高频 UI 变化 | 0 | 16 / 64 | DROP_OLDEST |
| 不能丢的业务事件 | 0 | 适当设置 | SUSPEND |
| 只保留最新值(不 replay) | 0 | 1 | DROP_OLDEST |
| 只保留最新值(类似 StateFlow) | 1 | 0 | SUSPEND |
| 页面状态 | 不建议 | 不建议 | 不建议,优先 StateFlow |
一点想法
SharedFlow 的参数真不是为了故弄玄虚,让 API 看起来复杂,而是为了让它能覆盖不同的事件模型。
replay 决定旧事件要不要给新订阅者。
extraBufferCapacity 决定发送方能不能先把事件放进缓冲区。
onBufferOverflow 决定事件太多时(出现了背压),是等一等,丢旧的,还是丢新的。
其实,Flow 在整个 Kotlin 协程体系中扮演着至关重要的角色。它不仅仅是 Android 开发中处理异步数据流的利器,更是协程生态里连接“数据生产”与“数据消费”的核心桥梁。从冷流到热流,从 StateFlow 到 SharedFlow,Kotlin 协程通过 Flow 提供了一套统一、优雅且线程安全的响应式编程方案,让复杂的异步逻辑变得可组合、可测试、可预测。
当然,Flow 的强大离不开协程本身对线程调度的良好支持。
而在 Android 开发中,线程(后台任务)处理的发展历程同样是一段值得回味的演进史——从早期的 AsyncTask、HandlerThread,到后来的 RxJava,再到如今的协程和 Flow。后续我会专门写一篇文章,和大家聊聊 Android 线程处理的过去、现在与未来,敬请期待。