很多 Android 开发者使用 stateIn() 时,代码几乎都是以下这种:
kotlin
stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UiState.Loading
)
这其实已经是标准写法了。
实际上:
text
stateIn 做的事情只有两件:
1、把冷流升级为热流
2、通过 SharingStarted 控制上游生命周期
这里最关键的其实是
text
SharingStarted
它决定:
- 上游什么时候启动
- 上游什么时候停止
- 缓存什么时候失效
- 页面退出后资源是否释放
- 页面重建时是否重新请求数据
某种意义上:
SharingStarted 才是 Flow 世界里的生命周期管理器。
一、stateIn 的本质
假设有这样一个 Repository:
kotlin
fun userFlow(): Flow<User> = flow {
println("开始请求用户信息")
emit(api.getUser())
}
这是一个标准冷流。
意味着:
text
collect 一次
执行一次
项目中经常会出现这种情况:
页面顶部头像需要用户信息:
页面中间的用户卡片也需要用户信息:
此时:
text
Avatar collect 一次
Profile collect 一次
日志:
text
开始请求用户信息
开始请求用户信息
因为:
text
Flow 默认是冷流
每次 collect
都会重新执行整个上游
于是:
kotlin
val user = repository.userFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = User.Empty
)
现在:
text
多个 Collector
共享同一个上游
日志:
text
开始请求用户信息
无论:
- 1 个 Collector
- 2 个 Collector
- 10 个 Collector
都共享同一个上游执行结果。
实际上, 在Room的场景:
kotlin
val messages = dao.observeMessages()
可能同时存在:
- 聊天列表 collect
- 未读角标 collect
- 消息统计 collect
如果没有 stateIn():
text
Room 注册 3 次 Observer
SQLite 执行 3 次查询
使用 stateIn() 后:
text
Room 只监听一次
多个消费者共享结果
这其实才是 stateIn() 最核心的价值:
把昂贵的上游操作共享给多个消费者。
二、真正的核心:SharingStarted
官方提供了三种策略:
| 策略 | 启动时机 | 停止时机 | 适用场景 |
|---|---|---|---|
| Eagerly | 立即启动 | Scope 销毁 | 全局数据 |
| Lazily | 首次订阅启动 | Scope 销毁 | 懒加载缓存 |
| WhileSubscribed | 有订阅启动 | 无订阅超时后停止 | UI 数据 |
三、Eagerly:项目启动立即工作
kotlin
val config = repository.configFlow()
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
Config.Empty
)
特点:
text
ViewModel 创建
立即启动上游
即使:
text
没有任何 Collector
上游仍然持续运行。
例如:
text
Application 启动
↓
ViewModel 创建
↓
网络开始请求
↓
页面甚至还没打开
适用于:
- 登录状态
- 用户信息
- 配置中心
本质上:
text
数据比 UI 更重要
四、Lazily:第一次使用时启动
kotlin
val userInfo = repository.userFlow()
.stateIn(
viewModelScope,
SharingStarted.Lazily,
User.Empty
)
特点:
text
没人订阅
不上班
有人订阅
开始工作
订阅结束
继续工作
只会启动一次。
以后永不停止。
生命周期:
text
第一次 collect
↓
启动上游
↓
页面退出
↓
继续运行
↓
ViewModel 销毁
↓
结束
适合:
- 大缓存数据
- 初始化较重的数据
- 不希望重复初始化的数据源
例如:
- 地图 SDK
- 播放器状态
- 大型配置数据
五、WhileSubscribed:UI 场景最优解
这是官方最推荐的方案。
kotlin
val uiState = repository.dataFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = UiState.Loading
)
行为:
text
有页面观察
↓
启动上游
页面退出
↓
停止上游
非常符合:
text
UI 生命周期
因此:
大部分 ViewModel 都应该优先考虑 WhileSubscribed。
六、容易被忽略的两个参数
很多人用了几年:
kotlin
SharingStarted.WhileSubscribed(5000)
却不知道这两个参数到底干什么。
完整定义:
kotlin
WhileSubscribed(
stopTimeoutMillis,
replayExpirationMillis
)
1、stopTimeoutMillis
例如:
kotlin
SharingStarted.WhileSubscribed(
stopTimeoutMillis = 5000
)
意思:
text
最后一个订阅者消失后
继续存活 5 秒
生命周期:
text
Activity A collect
↓
用户旋转屏幕
↓
旧 Activity 销毁
↓
没有订阅者
↓
等待 5 秒
↓
新 Activity 建立
↓
重新订阅
由于:
text
5 秒内重新订阅
所以上游不会停止。
避免了:
- 网络重新请求
- 数据库重新监听
- Flow 重新启动
这实际上是一种:
text
防抖机制
如果设置:
kotlin
WhileSubscribed(0)
那么:
text
页面退出
立即停止
页面重建
重新启动
旋转一次屏幕:
text
启动
停止
启动
停止
启动
日志可能会非常夸张。
2、replayExpirationMillis
例如:
kotlin
WhileSubscribed(
stopTimeoutMillis = 5000,
replayExpirationMillis = 0
)
它控制的是:
text
缓存什么时候失效
默认值:
text
Long.MAX_VALUE
意味着:
text
缓存永不过期
假设:
kotlin
val flow = flow {
delay(2000)
emit(Random.nextInt())
}
第一次进入页面:
text
Loading
↓
42
退出页面。
重新进入:
text
42
↓
重新请求
↓
88
因为:
text
42 被缓存下来了
所以用户会先看到旧值。
如果:
kotlin
WhileSubscribed(
stopTimeoutMillis = 5000,
replayExpirationMillis = 0
)
行为变成:
text
页面退出
↓
缓存立即清空
↓
重新进入页面
↓
显示 initialValue
↓
重新请求
↓
88
日志:
text
0
↓
88
旧值不会再出现。
七、什么时候应该设置 replayExpirationMillis = 0?
适合:
- 实时股票
- 实时价格
- 订单状态
- 设备状态
- 在线人数
- 倒计时
这些数据:
text
旧值没有意义
用户看到旧数据反而是一种错误。
而下面这些场景:
- 用户资料
- 配置中心
- 主题模式
- 设置项
- 登录信息
缓存反而是好事。
因为:
text
用户不希望看到闪屏和 Loading
八、推荐配置
绝大多数 ViewModel:
kotlin
stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(
stopTimeoutMillis = 5000
),
initialValue = UiState.Loading
)
实时数据:
kotlin
stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(
stopTimeoutMillis = 5000,
replayExpirationMillis = 0
),
initialValue = UiState.Empty
)
全局状态:
kotlin
stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = InitialState
)

源码: Github