Kotlin SharedFlow 的三个参数到底有啥用

这周同事跟我吃饭的时候,谈到了我前几天写的文章,对于里面的 LiveData 颇有同感,不过他说虽然用了几年 SharedFlow,但是一直没有搞懂那三个参数到底什么意思。

我跟他说,没事儿,小菜一碟!

那么,今天,我们就好好讲讲 SharedFlow 三个参数的含义,话不多说,正文开始。

SharedFlow 是 Kotlin Coroutines 中非常重要的一个 API,尤其是在 Android 开发里,它经常被用来替代过去 LiveData + EventWrapper 的一次性事件写法。

但是,SharedFlow 并不是一个"开箱即用就一定正确"的事件流。它的行为很大程度上取决于几个关键参数:

kotlin 复制代码
MutableSharedFlow<T>(
    replay = 0,
    extraBufferCapacity = 0,
    onBufferOverflow = BufferOverflow.SUSPEND
)

这三个参数分别决定了:

kotlin 复制代码
replay                 // 新订阅者能收到几个旧值
extraBufferCapacity    // 除 replay 之外,额外给发送方多少缓冲
onBufferOverflow       // 缓冲区满了以后怎么办

理解这几个参数,是正确使用 SharedFlow 的关键。

粗略介绍 SharedFlow

我相信大部分开发者对这个 SharedFlow 已经非常熟悉了,但我还是要稍微介绍一下。

SharedFlow 是 Kotlin Coroutines 提供的一种热流。

普通 Flow 是冷流,只有被 collect 的时候才会开始执行。而 SharedFlow 更像一个广播器:事件可以被发送出去,也可以被多个 collector 同时接收。

最常见的写法是:

kotlin 复制代码
private val _events = MutableSharedFlow<UiEvent>()
val events = _events.asSharedFlow()

然后在 ViewModel 中发送事件:

kotlin 复制代码
viewModelScope.launch {
    _events.emit(UiEvent.ShowToast("保存成功"))
}

在 UI 层收集事件:

kotlin 复制代码
lifecycleScope.launch {
    viewModel.events.collect { event ->
        handleEvent(event)
    }
}

这看起来很简单,不过一旦你开始用 SharedFlow,你会发现你会碰到很多小 Bug。

搞清楚 SharedFlow,你需要问自己这几个问题:

  • 这个事件要不要保存?
  • 新订阅者要不要收到旧事件?
  • 发送太快时要不要缓冲?
  • 缓冲区满了以后怎么办?

这些问题,正是 MutableSharedFlow 参数要解决的。

默认行为

如果你这样写:

kotlin 复制代码
val flow = MutableSharedFlow<String>()

它等价于:

kotlin 复制代码
val flow = MutableSharedFlow<String>(
    replay = 0,
    extraBufferCapacity = 0,
    onBufferOverflow = BufferOverflow.SUSPEND
)

这意味着它是一个:没有 replay、没有额外 buffer、缓冲区满了就挂起发送方的 SharedFlow。

在这种情况下:

kotlin 复制代码
flow.emit("Hello")

如果当前没有 collector,这个值不会被保存下来。因为 replay = 0,后面再来的订阅者也收不到它,相当于这个值凭空消失了。

只有当前有 collector 的时候,这个值才能被正常收到!

所以默认的 MutableSharedFlow() 更像是:有人听的时候,我就同步广播;没人听的时候,这个事件就过去了。

这也是为什么你在写 UI 事件时,不能只知道 SharedFlow 可以发事件,还要知道它到底有没有缓冲、有没有 replay。

replay

replay 表示:新订阅者开始 collect 时,可以收到最近几个已经发送过的值。

比如:

kotlin 复制代码
val flow = MutableSharedFlow<Int>(
    replay = 2
)

如果已经发送过:

kotlin 复制代码
flow.emit(1)
flow.emit(2)
flow.emit(3)

然后一个新的 collector 才开始 collect:

kotlin 复制代码
flow.collect {
    println(it)
}

它会先收到最近的两个值:

text 复制代码
2
3

所以 replay 的作用就是:给新订阅者重放历史值。

那么 replay 适合什么场景呢?

1. 希望新订阅者拿到最近的数据

比如一个新闻刷新流:

kotlin 复制代码
val latestNews = MutableSharedFlow<List<News>>(
    replay = 1
)

新的页面订阅时,可以马上拿到最近一次新闻列表。

不过,如果你只是想获取"当前状态",一般更推荐使用 StateFlow。因为 StateFlow 本身就是为状态设计的。

2. 希望事件具有"粘性"

比如:

kotlin 复制代码
val loginEvent = MutableSharedFlow<LoginResult>(
    replay = 1
)

如果登录结果已经发出,页面旋转之后重新 collect,还能收到最近一次登录结果。

但这也是危险点。

如果你用它发送 Toast、Snackbar、导航这类一次性事件,replay = 1 可能会导致事件重复消费。

这个当,可能很多开发者都上过。

比如:

kotlin 复制代码
val events = MutableSharedFlow<UiEvent>(
    replay = 1
)

发送:

kotlin 复制代码
_events.emit(UiEvent.ShowToast("保存成功"))

页面旋转后重新 collect,可能又收到上一次 Toast 事件,导致 Toast 再弹一次。

所以:

kotlin 复制代码
replay = 1

适合"新订阅者需要旧数据"的场景,不适合普通一次性 UI 事件。

选择建议

一般可以这样理解:

kotlin 复制代码
replay = 0

表示:不给新订阅者补发旧值。适合以下场景:

  • Toast
  • Snackbar
  • 导航事件
  • 点击事件
  • 一次性弹窗事件

而:

kotlin 复制代码
replay = 1

表示:新订阅者可以收到最近一次值。适合:

  • 最近一次结果
  • 最近一次配置
  • 最近一次广播数据
  • 需要"粘性"的事件

如果你发现自己写:

kotlin 复制代码
MutableSharedFlow(replay = 1)

只是为了保存页面状态,那最好考虑一下 MutableStateFlow。

关于 replay 的一个小规律:当你想把 SharedFlow 当做事件,replay 给 0,想把 SharedFlow 当状态,replay 给 1

extraBufferCapacity

这个参数又有什么用呢?

extraBufferCapacity 表示:在 replay 缓存之外,额外给 SharedFlow 增加多少缓冲空间。

比如:

kotlin 复制代码
val flow = MutableSharedFlow<String>(
    replay = 0,
    extraBufferCapacity = 1
)

这表示:它不会把旧值重放给新订阅者,但可以在 collector 暂时没来得及处理时,额外缓冲 1 个值。

这点和 replay 最大的区别是:会影响新订阅者能不能收到旧值。

extraBufferCapacity 主要影响发送方会不会被挂起,以及慢订阅者来不及处理时能不能暂存。

也就是说,extraBufferCapacity 的主要作用是处理背压问题!

replay vs extraBufferCapacity

这两个参数都和"缓存"有关,但目的不同。

参数 作用 新订阅者能收到吗
replay 决定新订阅者能收到最近几个值 能,最多收到 replay 个
extraBufferCapacity 额外缓冲空间,避免发送方马上挂起 单独设置时不参与 replay,但与 replay 配合时总缓冲可能参与

比如:

kotlin 复制代码
MutableSharedFlow<String>(
    replay = 1,
    extraBufferCapacity = 0
)

含义是:保存最近 1 个值,新订阅者可以收到。

而:

kotlin 复制代码
MutableSharedFlow<String>(
    replay = 0,
    extraBufferCapacity = 1
)

含义是:不给新订阅者补发旧值,但允许发送方多塞 1 个还没被处理的值。

所以对于一次性 UI 事件,很多时候会这样写:

kotlin 复制代码
private val _events = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 1
)

这样做的好处是:避免页面重新订阅后重复消费旧事件,同时允许事件在 collector 暂时没准备好时,有一个小缓冲空间。

extraBufferCapacity = 0

如果:

kotlin 复制代码
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 应该怎么办?

它常见有三个取值:

kotlin 复制代码
BufferOverflow.SUSPEND
BufferOverflow.DROP_OLDEST
BufferOverflow.DROP_LATEST

默认值是 BufferOverflow.SUSPEND。

1. SUSPEND

kotlin 复制代码
MutableSharedFlow<Int>(
    replay = 0,
    extraBufferCapacity = 1,
    onBufferOverflow = BufferOverflow.SUSPEND
)

SUSPEND 的含义是:如果缓冲区满了,发送方挂起,等 collector 消费。

适合不能丢事件的场景,例如

  • 订单状态事件
  • 支付流程事件
  • 必须按顺序处理的业务事件

它的特点是安全,但可能让发送方(上游)等待。

2. DROP_OLDEST

它的含义是:缓冲区满了以后,丢掉最旧的值,放入最新的值。

适合:只关心最新事件的场景,比如:

  • 搜索关键词变化
  • 滚动位置变化
  • 进度刷新
  • 高频传感器数据

这类场景里,旧值过期很快。

比如用户快速输入:

text 复制代码
a
an
and
andr
andro
android

如果处理不过来,保留最新的 android 通常比保留最早的 a 更有意义。

3. DROP_LATEST

它的含义是:缓冲区满了以后,丢掉最新来的值,保留已有缓冲。

适合:当前正在处理的东西更重要,新来的可以暂时忽略,比如:

  • 正在处理上一个请求时,新请求到来可以暂时丢弃
  • 某些日志采样,保留已有记录,新采样可忽略
  • 批量处理任务中,当前批次未完成时新任务可延后

不过在 UI 事件中,DROP_LATEST 用得相对少一些。因为很多时候我们更倾向于保留最新值。

在一起

现在,把三个参数放到一起讨论。可以把 MutableSharedFlow 想象成一个事件管道。

kotlin 复制代码
MutableSharedFlow<T>(
    replay = A,
    extraBufferCapacity = B,
    onBufferOverflow = C
)

其中:replay = A 决定:新订阅者能不能拿到旧值,能拿几个。

extraBufferCapacity = B 决定:发送方最多可以额外缓冲几个值,而不必马上挂起(如果你设置的是挂起的话)。

onBufferOverflow = C 决定:缓冲区满了以后,是挂起,丢旧的,还是丢新的。

所以,如果单从缓存来看,SharedFlow 总缓冲能力可以粗略理解为:

kotlin 复制代码
总缓冲 = replay + extraBufferCapacity

但是这两个空间的语义不同:

  • replay 部分会给新订阅者重放。
  • extraBufferCapacity 部分不会给新订阅者重放。

实战场景

1. 一次性 UI 事件

例如:Toast、Snackbar、Navigation 等推荐:

kotlin 复制代码
private val _events = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 1
)
val events = _events.asSharedFlow()

发送:

kotlin 复制代码
fun save() {
    viewModelScope.launch {
        _events.emit(UiEvent.ShowToast("保存成功"))
    }
}

收集:

kotlin 复制代码
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. 最近一次结果

推荐:

kotlin 复制代码
private val _latestResult = MutableSharedFlow<Result<Data>>(
    replay = 1
)
val latestResult = _latestResult.asSharedFlow()

适合:新订阅者需要马上拿到最近一次结果

比如:

  • 网络请求结果
  • 最近一次同步状态
  • 最近一次定位结果

但如果这是 UI 状态,更推荐 MutableStateFlow()。

本篇文章我提到过了很多次的 MutableStateFlow,这个对比,我放到了文章的后部分,各位耐心看完即可。

3. 高频事件流

比如搜索框输入、滑动位置、进度更新。

推荐:

kotlin 复制代码
private val _queryChanges = MutableSharedFlow<String>(
    replay = 0,
    extraBufferCapacity = 64,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

含义是:不重放旧值,但允许一定缓冲(这个缓冲的值可以按照业务而定,64 只是一个假设值);如果处理不过来,丢掉旧值,保留新值。

适合:

  • 搜索输入
  • 滚动事件
  • 拖拽事件
  • 实时进度

这种场景一般不要求每一个中间值都被处理,但新值通常比旧值更重要。

4. 不能丢的业务事件

推荐:

kotlin 复制代码
private val _events = MutableSharedFlow<BusinessEvent>(
    replay = 0,
    extraBufferCapacity = 16,
    onBufferOverflow = BufferOverflow.SUSPEND
)

含义是:可以缓冲一部分事件,但满了以后不要丢,发送方等待。适合:

  • 订单事件
  • 支付事件
  • 关键业务状态变化
  • 需要按顺序处理的任务

SharedFlow 和 StateFlow

很多人容易把 SharedFlow 和 StateFlow 混在一起。

可以用一句话区分:

StateFlow 表示"现在是什么状态"。
SharedFlow 表示"发生了什么事件"。

比如页面状态:

kotlin 复制代码
data class UiState(
    val loading: Boolean = false,
    val data: List<Item> = emptyList(),
    val error: String? = null
)

更适合:

kotlin 复制代码
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

而 Toast、Snackbar、导航:

kotlin 复制代码
sealed interface UiEvent {
    data class ShowToast(val message: String) : UiEvent
    data class Navigate(val route: String) : UiEvent
}

更适合:

kotlin 复制代码
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 线程处理的过去、现在与未来,敬请期待。

相关推荐
aqi0010 小时前
鸿蒙版本的电子书阅读APP开放源码啦
android·华为·ai编程·harmonyos·鸿蒙
墨天梦10 小时前
B19_SunnyWeather到阅读客户端
android·kotlin
美狐美颜sdk11 小时前
直播APP源码可以直接接入视频美颜sdk吗?技术方案详解
android·人工智能·音视频·美颜sdk·直播美颜sdk
用户693717500138414 小时前
2026,程序员的时代拐点到了
android·前端·后端
浪潮IT馆16 小时前
Android Studio 非最新版本下载安装教程
android·ide·android studio
xiaozongt198917 小时前
MasterGo + Claude Code 生成 Android 布局:完整实践指南
android
Anhty18 小时前
2026最新免费手机音频处理工具 !!
android·功能测试·ios·智能手机·音视频
2501_9159090620 小时前
iOS应用从开发到上架App Store的完整发布流程与步骤指南
android·ios·小程序·https·uni-app·webview
狗凯之家源码网21 小时前
AI 步数修改提交系统源码应用场景与落地指南
android·数据库·人工智能·运动步数
ZHOUPUYU21 小时前
PHP 9.0 前瞻:下一代 PHP 会带来哪些新功能?
android·开发语言·php