很多 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》 是转载文章,点击查看原文。
