StateFlow 与 SharedFlow:Google 为什么要设计两套 Flow?—— 从一次 tryEmit(false) 到 WindowLeaked,彻底理解 Flow 的设计思想
2026/6/17 2:15:57 网站建设 项目流程

大家在学习 Kotlin Flow 的时候,经常会遇到两个类:

StateFlow SharedFlow

很多教程都会告诉你:

StateFlow 用于状态 SharedFlow 用于事件

但问题来了:

为什么 Google 不直接设计一个 Flow?

为什么非要拆成两套?

说实话 以前我也只是:会用

直到最近项目里踩了一个坑:

tryEmit() 返回 false

然后一路排查下去,

最后竟然牵出了:

SharedFlow StateFlow replay buffer Lifecycle WindowLeaked

最终让我真正理解了:

Google 为什么要设计两套 Flow。


一、一个奇怪的问题

项目中有这样一个事件流:

private val _tenantIdEvent = MutableSharedFlow<Int>() val tenantIdEvent = _tenantIdEvent.asSharedFlow()

获取租户失败时:

_tenantIdEvent.tryEmit(0)

结果日志打印:

false

更离谱的是:

UI 明明已经开始监听:

mViewModel.tenantIdEvent.collect { ... }

按理说:

有人收 为什么发不出去?

二、我最开始的理解是错的

很多人(包括我)会天然认为:

collect了 = 事件一定能收到

实际上:

collect存在 ≠ tryEmit一定成功

这才是问题的根源。


三、SharedFlow 到底是什么

先看看这段代码:

MutableSharedFlow<Int>()

很多人以为:

创建了一个事件流

实际上它等价于:

MutableSharedFlow<Int>( replay = 0, extraBufferCapacity = 0 )

即:

无缓存 无重放

四、emit 与 tryEmit 的本质区别

这是本次踩坑最大的收获之一。


emit

emit(value)

特点:

保证发送 必要时等待

可以理解为:

我必须把快递送到你手里

如果对方没准备好:

我等

tryEmit

tryEmit(value)

特点:

尝试发送 绝不等待 可能失败

可以理解为:

我敲一下门 能接收就接收 接不了我就走

所以:

tryEmit返回false 不是异常 而是发送失败

五、为什么 collect 了还会失败?

因为:

collect存在 ≠ 当前时刻能立即消费

而:

tryEmit()

又不愿意等待。

所以:

无缓冲SharedFlow + tryEmit = 极容易返回false

这也是很多人第一次接触 SharedFlow 时最容易踩的坑。


六、replay 与 buffer 到底有什么区别

很多人学 SharedFlow,

最容易混淆两个参数:

replay extraBufferCapacity

replay

作用:

给未来的新订阅者看

例如:

replay = 1

表示:

保存最近一次数据

新的 collector 进入时:

自动收到最近一次数据

因此:

replay决定粘性

extraBufferCapacity

作用:

给当前事件排队

例如:

extraBufferCapacity = 1

表示:

当前没人接 先暂存一下

因此:

buffer决定是否容易丢事件

七、我终于理解了 StateFlow

到这里我突然意识到:

Google 设计:

StateFlow SharedFlow

根本不是提供两个 API。

而是在解决两类完全不同的问题。


八、StateFlow 解决什么问题?

StateFlow 解决的是:

现在是什么状态?

例如:

loading pageState userInfo networkState

代码:

private val _isLoading = MutableStateFlow(false)

特点:

永远有当前值 永远保存最新状态

所以:

后来进入页面的人 也应该知道当前状态

九、为什么 StateFlow 天生有“粘性”?

因为:

状态本来就应该被记住

例如:

_isLoading.value = true

即使:

UI稍后才开始collect

仍然能够收到:

true

因为:

StateFlow保存的是状态

而不是事件。


十、SharedFlow 解决什么问题?

SharedFlow 解决的是:

刚刚发生了什么?

例如:

Toast Navigation ErrorEvent LoginSuccessEvent

这些东西本质都是:

一次性事件

例如:

登录成功

只发生一次。

不应该:

页面重建后再执行一次

十一、为什么 SharedFlow 默认不粘性?

想象一下:

Toast

如果重放:

旋转屏幕后 又弹一次

显然不合理。

再比如:

跳转页面

如果重放:

重新进入页面 又跳一次

直接出事故。

所以:

SharedFlow默认不粘性

这是设计使然。


十二、企业项目中的推荐配置

对于 UI Event:

private val _event = MutableSharedFlow<Event>( replay = 0, extraBufferCapacity = 1 )

原因:

不粘性 不容易丢事件

非常适合:

Toast Error Navigation LoginSuccess

十三、又踩了一个坑:WindowLeaked

就在以为问题结束的时候,

项目又报了:

WindowLeaked

日志显示:

Activity已经finish Dialog还活着

十四、真正的问题并不是 Dialog

最开始以为:

Dialog有Bug

后来发现:

真正的问题是顺序。

错误顺序:

请求完成 ↓ successBlock ↓ finish() ↓ loading=false ↓ dismissDialog

这时候:

Activity已经销毁 Dialog还没关闭

直接:

WindowLeaked

十五、正确顺序

应该是:

请求完成 ↓ loading=false ↓ dismissDialog ↓ successBlock ↓ finish()

这本质上是:

Lifecycle问题

而不是:

Flow问题

十六、Flow 背后真正的设计思想

到这里我终于明白了:

Google 其实是在引导开发者建立三个模型。


State(状态)

解决:

现在是什么

例如:

loading userInfo pageState

对应:

StateFlow

Event(事件)

解决:

刚刚发生了什么

例如:

Toast Navigation ErrorEvent

对应:

SharedFlow

Lifecycle(生命周期)

解决:

页面是否还活着

例如:

Dialog Activity Fragment

十七、总结

StateFlow

负责:

状态

例如:

loading pageState userInfo

特点:

有当前值 允许粘性 状态模型

SharedFlow

负责:

事件

例如:

Toast Navigation ErrorEvent LoginSuccessEvent

推荐:

MutableSharedFlow( replay = 0, extraBufferCapacity = 1 )

特点:

一次性事件 默认不粘性 事件模型

结语

以前我以为:

StateFlow 和 SharedFlow 只是两个不同的 API。

直到一次:

tryEmit(false) WindowLeaked

的排查过程,我才意识到:

Google 设计的从来不是两种 Flow。

而是在引导开发者区分:

  • 状态(State)
  • 事件(Event)
  • 生命周期(Lifecycle)

真正理解这三个模型,才算真正理解 Kotlin Flow。

tips:回顾思考:

以前我一直觉得 LiveData 已经够用了,StateFlow 和 SharedFlow 只是换了个 API。

直到这次踩坑,我才发现:

LiveData 更像是一个“观察数据变化”的工具。

而 Flow 则是在引导开发者建立State(状态)Event(事件)Lifecycle(生命周期)三种不同的思维模型。

真正理解 StateFlow 与 SharedFlow,并不是学会两个 API,而是在学习如何正确地表达状态与事件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询