StateFlow 与 SharedFlow:Google 为什么要设计两套 Flow?—— 从一次 tryEmit(false) 到 WindowLeaked,彻底理解 Flow 的设计思想
大家在学习 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 extraBufferCapacityreplay
作用:
给未来的新订阅者看例如:
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对应:
StateFlowEvent(事件)
解决:
刚刚发生了什么例如:
Toast Navigation ErrorEvent对应:
SharedFlowLifecycle(生命周期)
解决:
页面是否还活着例如:
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,而是在学习如何正确地表达状态与事件。
