Kotlin Flow 深入解析:`stateIn()` 的真正核心,其实是 SharingStarted

作者:潜龙勿用之化骨龙日期:2026/7/13

很多 Android 开发者使用 stateIn() 时,代码几乎都是以下这种:

1stateIn(
2    scope = viewModelScope,
3    started = SharingStarted.WhileSubscribed(5000),
4    initialValue = UiState.Loading
5)
6

这其实已经是标准写法了。

实际上:

1stateIn 做的事情只有两件:
2
31、把冷流升级为热流
42、通过 SharingStarted 控制上游生命周期
5

这里最关键的其实是

1SharingStarted
2

它决定:

  • 上游什么时候启动
  • 上游什么时候停止
  • 缓存什么时候失效
  • 页面退出后资源是否释放
  • 页面重建时是否重新请求数据

某种意义上:

SharingStarted 才是 Flow 世界里的生命周期管理器。


一、stateIn 的本质

假设有这样一个 Repository:

1fun userFlow(): Flow<User> = flow {
2    println("开始请求用户信息")
3    emit(api.getUser())
4}
5

这是一个标准冷流。

意味着:

1collect 一次
2执行一次
3

项目中经常会出现这种情况:

页面顶部头像需要用户信息:

页面中间的用户卡片也需要用户信息:

此时:

1Avatar collect 一次
2Profile collect 一次
3

日志:

1开始请求用户信息
2开始请求用户信息
3

因为:

1Flow 默认是冷流
2
3每次 collect
4都会重新执行整个上游
5

于是:

1val user = repository.userFlow()
2    .stateIn(
3        scope = viewModelScope,
4        started = SharingStarted.WhileSubscribed(),
5        initialValue = User.Empty
6    )
7

现在:

1多个 Collector
2共享同一个上游
3

日志:

1开始请求用户信息
2

无论:

  • 1 个 Collector
  • 2 个 Collector
  • 10 个 Collector

都共享同一个上游执行结果。


实际上, 在Room的场景:

1val messages = dao.observeMessages()
2

可能同时存在:

  • 聊天列表 collect
  • 未读角标 collect
  • 消息统计 collect

如果没有 stateIn()

1Room 注册 3  Observer
2SQLite 执行 3 次查询
3

使用 stateIn() 后:

1Room 只监听一次
2多个消费者共享结果
3

这其实才是 stateIn() 最核心的价值:

把昂贵的上游操作共享给多个消费者。


二、真正的核心:SharingStarted

官方提供了三种策略:

策略启动时机停止时机适用场景
Eagerly立即启动Scope 销毁全局数据
Lazily首次订阅启动Scope 销毁懒加载缓存
WhileSubscribed有订阅启动无订阅超时后停止UI 数据

三、Eagerly:项目启动立即工作

1val config = repository.configFlow()
2    .stateIn(
3        viewModelScope,
4        SharingStarted.Eagerly,
5        Config.Empty
6    )
7

特点:

1ViewModel 创建
2立即启动上游
3

即使:

1没有任何 Collector
2

上游仍然持续运行。

例如:

1Application 启动
2
3ViewModel 创建
4
5网络开始请求
6
7页面甚至还没打开
8

适用于:

  • 登录状态
  • 用户信息
  • 配置中心

本质上:

1数据比 UI 更重要
2

四、Lazily:第一次使用时启动

1val userInfo = repository.userFlow()
2    .stateIn(
3        viewModelScope,
4        SharingStarted.Lazily,
5        User.Empty
6    )
7

特点:

1没人订阅
2不上班
3
4有人订阅
5开始工作
6
7订阅结束
8继续工作
9

只会启动一次。

以后永不停止。

生命周期:

1第一次 collect
2    
3启动上游
4    
5页面退出
6    
7继续运行
8    
9ViewModel 销毁
10    
11结束
12

适合:

  • 大缓存数据
  • 初始化较重的数据
  • 不希望重复初始化的数据源

例如:

  • 地图 SDK
  • 播放器状态
  • 大型配置数据

五、WhileSubscribed:UI 场景最优解

这是官方最推荐的方案。

1val uiState = repository.dataFlow()
2    .stateIn(
3        scope = viewModelScope,
4        started = SharingStarted.WhileSubscribed(),
5        initialValue = UiState.Loading
6    )
7

行为:

1有页面观察
2    
3启动上游
4
5页面退出
6    
7停止上游
8

非常符合:

1UI 生命周期
2

因此:

大部分 ViewModel 都应该优先考虑 WhileSubscribed。


六、容易被忽略的两个参数

很多人用了几年:

1SharingStarted.WhileSubscribed(5000)
2

却不知道这两个参数到底干什么。

完整定义:

1WhileSubscribed(
2    stopTimeoutMillis,
3    replayExpirationMillis
4)
5

1、stopTimeoutMillis

例如:

1SharingStarted.WhileSubscribed(
2    stopTimeoutMillis = 5000
3)
4

意思:

1最后一个订阅者消失后
2继续存活 5 
3

生命周期:

1Activity A collect
2    
3用户旋转屏幕
4    
5 Activity 销毁
6    
7没有订阅者
8    
9等待 5 
10    
11 Activity 建立
12    
13重新订阅
14

由于:

15 秒内重新订阅
2

所以上游不会停止。

避免了:

  • 网络重新请求
  • 数据库重新监听
  • Flow 重新启动

这实际上是一种:

1防抖机制
2

如果设置:

1WhileSubscribed(0)
2

那么:

1页面退出
2立即停止
3页面重建
4重新启动
5

旋转一次屏幕:

1启动
2停止
3启动
4停止
5启动
6

日志可能会非常夸张。


2、replayExpirationMillis

例如:

1WhileSubscribed(
2    stopTimeoutMillis = 5000,
3    replayExpirationMillis = 0
4)
5

它控制的是:

1缓存什么时候失效
2

默认值:

1Long.MAX_VALUE
2

意味着:

1缓存永不过期
2

假设:

1val flow = flow {
2    delay(2000)
3    emit(Random.nextInt())
4}
5

第一次进入页面:

1Loading
2
342
4

退出页面。

重新进入:

142
2
3重新请求
4
588
6

因为:

142 被缓存下来了
2

所以用户会先看到旧值。

如果:

1WhileSubscribed(
2    stopTimeoutMillis = 5000,
3    replayExpirationMillis = 0
4)
5

行为变成:

1页面退出
2
3缓存立即清空
4
5重新进入页面
6
7显示 initialValue
8
9重新请求
10
1188
12

日志:

10
2
388
4

旧值不会再出现。


七、什么时候应该设置 replayExpirationMillis = 0?

适合:

  • 实时股票
  • 实时价格
  • 订单状态
  • 设备状态
  • 在线人数
  • 倒计时

这些数据:

1旧值没有意义
2

用户看到旧数据反而是一种错误。

而下面这些场景:

  • 用户资料
  • 配置中心
  • 主题模式
  • 设置项
  • 登录信息

缓存反而是好事。

因为:

1用户不希望看到闪屏和 Loading
2

八、推荐配置

绝大多数 ViewModel:

1stateIn(
2    scope = viewModelScope,
3    started = SharingStarted.WhileSubscribed(
4        stopTimeoutMillis = 5000
5    ),
6    initialValue = UiState.Loading
7)
8

实时数据:

1stateIn(
2    scope = viewModelScope,
3    started = SharingStarted.WhileSubscribed(
4        stopTimeoutMillis = 5000,
5        replayExpirationMillis = 0
6    ),
7    initialValue = UiState.Empty
8)
9

全局状态:

1stateIn(
2    scope = applicationScope,
3    started = SharingStarted.Eagerly,
4    initialValue = InitialState
5)
6

源码:Github


Kotlin Flow 深入解析:stateIn() 的真正核心,其实是 SharingStarted》 是转载文章,点击查看原文


相关推荐


Claude Sonnet 5 上线:别再让 Claude Code 一律烧 Opus
鲁大猿2026/7/5

Claude Sonnet 5 上线:别再让 Claude Code 一律烧 Opus 6 月 30 日,Anthropic 发布 Claude Sonnet 5。 如果你平时用 Claude Code,这条消息不应该只理解成“又来了一个更强模型”。 它真正影响的是一个更具体的团队决策:以后 Claude Code 到底什么时候用 Sonnet,什么时候用 Opus,什么时候必须让人接管? 我的判断很直接: 不要全员默认 Opus。 也不要一听 Sonnet 5 便宜、上下文长,就把所有 Ag


你好,我叫Token——AI世界里最忙的搬砖工
Kfaino2026/6/27

码农的AI翻身之旅(一) 你好,我叫Token——AI世界里最忙的搬砖工 大家好。 我叫 Token。 别看我名字洋气,其实我就是个打零工的。 AI世界里,所有人都认识ChatGPT,认识DeepSeek,认识Claude,认识Gemini…… 可是,没有几个人认识我。 然而,没有我,他们一句话都说不出来。 我的出生 某一天。 一个程序员打开了ChatGPT。 他说: 帮我写一个Spring Boot项目。 于是,我出生了。 准确来说,不是我一个。 而是一大群兄弟。 因为AI眼里,根本没


一篇看懂 VKE AI Profiling:AI 应用性能分析优化实战
火山引擎Agent社区2026/6/18

你有没有遇到过这样的情况: AI 模型在训练时 GPU 利用率忽高忽低,容器资源明明给够了,训练时长却总是超出预期?或者在推理服务上线后,延迟时不时抖动,重启一下又好了,但问题根本找不到? “我的模型在裸机上跑只要 2 小时,放到容器里怎么变成 3 小时了?” “资源都给了,CPU 和内存也没瓶颈,我不知道还要看什么。” 很多团队在模型效果验证通过后,真正进入上线或规模化使用时,往往会遇到一个共同问题:GPU 看起来很忙,但整体效率并不高;系统投入不少,性能瓶颈却不容易快速定位。 这正是 A


架构视图与文档:C4 模型从入门到实战
ltl2026/6/10

你上次打开团队的架构图是什么时候? 大多数团队都有架构图。Confluence 上躺着一张两年前画的系统拓扑图,Visio 文件在某个共享盘里,PowerPoint 里有几页"技术方案评审"的框线图。问题是:没人信它们。新人入职时看一眼,发现和实际系统对不上,从此再也不看。老人心里有一张"真正的架构图",但那张图只存在于他的脑子里。 这不是个别现象。Simon Brown 在 2018 年的调查中发现,超过半数的开发者认为自己团队的架构文档"基本没用"或"严重过时"(Simon Brown, "


Java MyBatis-Flex 实战指南:从 BaseMapper 到 QueryWrapper 的轻量 ORM 用法
唐青枫2026/6/3

简介 MyBatis-Flex 是一个基于 MyBatis 的增强框架。 它的定位很直接: 保留 MyBatis 写 SQL 的灵活性,同时补上通用 CRUD、链式查询、分页、逻辑删除、乐观锁等常用能力。 普通 MyBatis 项目里,常见代码结构是: Entity Mapper 接口 Mapper XML Service Controller 如果只是做一张表的增删改查,也要写不少重复 SQL。 MyBatis-Flex 的思路是: 简单 CRUD 交给 BaseMapper 复杂条件交给


我用AI一小时撸了个单词学习站,每天自动生成5个单词
修己xj2026/5/26

AI编程热度居高不下,我也一直在用CodeBuddy辅助开发。最近发现了一个有意思的功能——Skill(技能包),灵机一动:不如写个技能,每天自动生成英文单词并部署成网页,这样打开浏览器就能学。 说干就干,全程AI辅助,一个多小时就搞定了。过程分享给大家。 以下是项目地址及在线地址 在线访问:xiuji008.github.io/everyday-en… 项目完全开源:github.com/xiuji008/ev… 灵感来源 CodeBuddy的Skill功能可以定义一套自动化流程。比如我说“


claudeCode+DeepSeekV4pro[1m]
冰镇生鲜2026/5/5

Claude Code 完美调和配置 这是经过多轮磨合后的全局配置,可以直接复制到新电脑的 ~/.claude/settings.json 使用。 也可以直接让 AI 处理一切。 完整配置文件 ~/.claude/settings.json { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-你的token", "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic", "AN


【AI Agent | 第七篇】Skill的使用:将经验沉淀成可复用工作流
程序员夏末2026/4/25

前言   此文是我近期学习Agent中关于Skill使用的一点心得,结合AI整理总结而来,分享给大家。一是为了记录,二是用于日后的回顾,三也是希望能给其他初学者带来一点点帮助。 目录 1. 为什么我会开始关注 Skill?2. Skill 到底解决了什么问题?3. 从写博客这个例子理解 Skill 的价值4. 一个 Skill 通常包含哪些内容?5. Skill 使用时会加载哪些上下文?6. 写 Skill 时我踩到的几个细节7. 什么样的工作流适合沉淀成 Skill?8. 总结


【AI工具篇】Windows 安装 WSL 全攻略:wsl --install 一键部署 + VSCode 搭配使用好处详解
*星星之火*2026/4/16

Windows 安装 WSL 全攻略:wsl --install 一键部署 + VSCode 搭配使用好处详解 前言 在 Windows 上做开发,尤其是后端、C/C++、Python、Docker、机器学习等开发时,经常会遇到环境不一致、命令不兼容、依赖难装等问题。 传统虚拟机笨重卡顿,双系统切换麻烦,而 WSL(Windows Subsystem for Linux) 完美解决了这些痛点。 本文详细介绍: Windows 安装 WSL 的好处一条命令 wsl --install 完成安装V


【docker】Ubuntu22使用skopeo离线推送镜像
s9123601012026/4/8

1,准备安装skopeo # 安装skopeo apt install skopeo --fix-missing # 创建目录 mkdir -p skopeo_bundle # 拷贝主要的 cp /usr/bin/skopeo skopeo_bundle/ # 下载依赖库 ldd /usr/bin/skopeo | awk '{print $3}' | grep '/' | xargs -I '{}' cp -v '{}' skopeo_bundle/ # 压缩 tar czf skopeo

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读