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

很多 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

相关推荐
心中有国也有家13 小时前
AtomGit Flutter 鸿蒙客户端:ModalBottomSheet 实战
android·javascript·学习·flutter·华为·harmonyos
YSoup15 小时前
Android Stuidio中下载TRAE插件依赖的JCEF后打不开
android
2401_8949155315 小时前
Geo搜索优化排名源码部署搭建全流程详解
android·开发语言·flask·php·精选
星释16 小时前
鸿蒙智能体开发实战:34.鸿蒙壁纸大师 - 提示词工程与优化
android·华为·harmonyos·鸿蒙
blanks202017 小时前
android 编译问题记录
android
bobuddy17 小时前
平台总线(platform bus)
android
plainGeekDev18 小时前
libs 目录 → Gradle 依赖管理
android·java·kotlin
帅次19 小时前
Android 高级工程师面试:Flutter Widget 体系 近1年高频追问 20 题
android·flutter·面试·element·widget·setstate·renderobject
帅次19 小时前
Android 高级工程师面试:Jetpack 核心 近1年高频追问 20 题
android·面试·协程·jetpack·stateflow·lifecycle