当前位置: 首页 > news >正文

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 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,而是在学习如何正确地表达状态与事件。

http://www.zskr.cn/news/1423041.html

相关文章:

  • 基于Arduino与MPU6050的模型火箭智能降落伞释放系统全解析
  • 终极指南:如何免费快速解码QQ音乐加密文件(qmcdump完整教程)
  • 基于ESP32与Node.js的物联网智能时钟:从架构设计到FreeRTOS任务调度
  • 别再手动调坐标了!OpenPnP导入Gerber/坐标文件后,用这3个Mark点搞定全板自动校正
  • Wallpaper Engine下载器:3步轻松获取Steam创意工坊动态壁纸的完整指南
  • 构建安全合规的大规模健康研究平台:FAIR原则与隐私计算实践
  • Aspose.Cells企业级应用实战:从License机制解析到合规批量处理方案设计
  • 零基础入门网页开发:HTML与CSS核心概念与实践指南
  • 构建可信机器学习算法:从可解释性、公平性到鲁棒性的工程实践
  • 告别iOS开发噩梦:如何用Xcode开发者磁盘映像解决版本不匹配问题
  • 从零打造复古智能手表:ESP32-S3与HCMS-2971的硬件开发全记录
  • ADI DSP开发者论坛实战:如何高效搜索SC589问题与获取官方支持(附中文关键词)
  • 手把手教你用Redriver芯片搞定USB4/PCIe Gen4信号衰减问题(附电路设计要点)
  • 学术写作中文献引用的规范与实践:从原理到工具全解析
  • Docker部署RabbitMQ后,你的Spring Boot项目连不上?可能是vhost权限在作祟
  • STM32 USB MSC实战避坑指南:解决W25Q64模拟U盘的速度与格式化问题
  • 如何免费观看Twitch订阅专属内容:终极无限制观看指南
  • 【限时开放】Claude文档生成企业级配置清单(含12个行业模板、8类安全合规校验规则、6套CI/CD集成脚本)
  • 免费在线音频转文字软件推荐:2026保姆级教程一看就会
  • yuzu模拟器完整教程:免费在PC上玩Switch游戏的终极指南
  • 基于Adafruit CPX与3D打印的智能交互直升机模型制作全攻略
  • [特殊字符] 书匠策AI:你的论文“私人门诊“开张了!教育博主实测全流程科普
  • 从零打造高扭矩太阳能小车:BO电机并联驱动与纸板结构实践
  • C语言新手必看:手把手教你写二进制转十进制的函数(附ZZULIOJ 1142题解)
  • 被97%用户关闭的Lindy隐藏开关,开启后自动拦截92%的BOM错配订单(实测数据+权限配置路径)
  • 最新长期支持版本nodejs安装及环境配置(保姆级图文+安装包)
  • P14076 [GESP202509 六级] 货物运输
  • 华为ENSP模拟器实战:手把手教你搭建一个带无线AP的校园网(含AC6005配置)
  • 避开理论深坑:手把手调试Buck电源环路,从仿真到实测的避雷指南
  • 别再只跑MS MARCO了!用BEIR基准给你的检索模型做个“零样本体检”(附实战避坑指南)