一 Kotlin Flow 中的 stateIn 和 shareIn
一、简单比喻理解
想象一个水龙头(数据源)和几个水杯(数据接收者):
- 普通 Flow(冷流):每个水杯来接水时,都要重新打开水龙头从头放水
- stateIn/shareIn(热流):水龙头一直开着,水存在一个水池里,任何水杯随时来接都能拿到水
二、stateIn 是什么?
就像手机的状态栏
- 总是显示最新的一条信息(有
当前值
) - 新用户打开手机时,立刻能看到最后一条消息
- 适合用来表示"当前状态",比如:
- 用户登录状态(已登录/未登录)
- 页面加载状态(加载中/成功/失败)
- 实时更新的数据(如股票价格)
代码示例:
// 创建一个永远知道当前温度的温度计
val currentTemperature = sensorFlow.stateIn(scope = viewModelScope, // 在ViewModel生命周期内有效started = SharingStarted.WhileSubscribed(5000), // 5秒无订阅就暂停initialValue = 0 // 初始温度0度)// 在Activity中读取(总是能拿到当前温度)
textView.text = "${currentTemperature.value}°C"
三、shareIn 是什么?
就像广播电台
- 不保存"当前值"(没有
.value
属性) - 新听众打开收音机时,可以选择:
- 从最新的一条新闻开始听(replay=1)
- 只听新新闻(replay=0)
- 适合用来处理"事件",比如:
- 显示Toast提示
- 页面跳转指令
- 一次性通知
代码示例:
// 创建一个消息广播站
val messages = notificationFlow.shareIn(scope = viewModelScope,started = SharingStarted.Lazily, // 有人收听时才启动replay = 1 // 新听众能听到最后1条消息)// 在Activity中收听广播
lifecycleScope.launch {messages.collect { msg ->Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()}
}
四、主要区别对比
特性 | stateIn (状态栏) | shareIn (广播电台) |
---|---|---|
有无当前值 | 有(.value 直接访问) | 无(必须通过collect接收) |
新订阅者 | 立即获得最新值 | 可配置获得最近N条(replay) |
典型用途 | 持续更新的状态(如用户积分) | 一次性事件(如"购买成功"提示) |
内存占用 | 始终保存最新值 | 按需缓存(可配置) |
是否热流 | 是 | 是 |
五、为什么要用它们?
-
节省资源:避免重复计算(多个界面可以共享同一个数据源)
- ❌ 不用时:每个界面都单独请求一次网络数据
- ✅ 使用后:所有界面共享同一份网络数据
-
保持一致性:所有订阅者看到的数据完全相同
- 比如用户头像更新后,所有界面立即同步
-
自动管理生命周期:
- 当Activity销毁时自动停止收集
- 当配置变更(如屏幕旋转)时保持数据不丢失
六、生活场景类比
场景1:微信群(stateIn)
- 群里最后一条消息就是当前状态(.value)
- 新成员进群立刻能看到最后一条消息
- 适合:工作群的状态同步
场景2:电台广播(shareIn)
- 主播不断发送新消息
- 听众打开收音机时:
- 可以设置是否听之前的回放(replay)
- 但无法直接问"刚才最后一首歌是什么"(无.value)
- 适合:交通路况实时播报
七、什么时候用哪个?
用 stateIn 当:
- 需要随时知道"当前值"
- 数据会持续变化且需要被多个地方使用
- 例如:
- 用户登录状态
- 购物车商品数量
- 实时位置更新
用 shareIn 当:
- 只关心新事件,不关心历史值
- 事件可能被多个接收者处理
- 例如:
- "订单支付成功"通知
- 错误提示消息
- 页面跳转指令
八、超简单选择流程图
要管理持续变化的状态吗?是 → 需要直接访问当前值吗?是 → 用 stateIn否 → 用 shareIn(replay=1)否 → 这是一次性事件吗?是 → 用 shareIn(replay=0)
记住这个简单的口诀:
“状态用state,事件用share,想要回放加replay”
二 Kotlin Flow 的 shareIn
和 stateIn
操作符完全指南
在 Kotlin Flow 的使用中,shareIn
和 stateIn
是两个关键的操作符,用于优化流的共享和状态管理。本教程将深入解析这两个操作符的使用场景、区别和最佳实践。
一、核心概念解析
1. 冷流 vs 热流
- 冷流 (Cold Flow):每个收集者都会触发独立的执行(如普通的
flow{}
构建器) - 热流 (Hot Flow):数据发射独立于收集者存在(如
StateFlow
、SharedFlow
)
2. 为什么需要 shareIn
/stateIn
?
- 避免对上游冷流进行重复计算
- 多个收集者共享同一个数据源
- 将冷流转换为热流以提高效率
二、stateIn
操作符详解
基本用法
val sharedFlow: StateFlow<Int> = flow {// 模拟耗时操作emit(repository.fetchData())
}.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = 0
)
参数说明:
- scope:共享流的协程作用域(通常用
viewModelScope
) - started:共享启动策略(后文详细讲解)
- initialValue:必须提供的初始值
特点:
- 总是有当前值(通过
value
属性访问) - 新收集者立即获得最新值
- 适合表示 UI 状态
使用场景示例:
用户个人信息状态管理
class UserViewModel : ViewModel() {private val _userState = repository.userUpdates() // Flow<User>.map { it.toUiState() }.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = UserState.Loading)val userState: StateFlow<UserState> = _userState
}
三、shareIn
操作符详解
基本用法
val sharedFlow: SharedFlow<Int> = flow {emit(repository.fetchData())
}.shareIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),replay = 1
)
参数说明:
- replay:新收集者接收的旧值数量
- extraBufferCapacity:超出 replay 的缓冲大小
- onBufferOverflow:缓冲策略(
SUSPEND
,DROP_OLDEST
,DROP_LATEST
)
特点:
- 可以有多个订阅者
- 没有
value
属性,必须通过收集获取数据 - 适合事件处理(如 Toast、导航事件)
使用场景示例:
全局事件通知
class EventBus {private val _events = MutableSharedFlow<Event>()val events = _events.asSharedFlow()suspend fun postEvent(event: Event) {_events.emit(event)}// 使用 shareIn 转换外部流val externalEvents = someExternalFlow.shareIn(scope = CoroutineScope(Dispatchers.IO),started = SharingStarted.Eagerly,replay = 0)
}
四、started
参数深度解析
1. SharingStarted.Eagerly
- 行为:立即启动,无视是否有收集者
- 用例:需要预先缓存的数据
- 风险:可能造成资源浪费
started = SharingStarted.Eagerly
2. SharingStarted.Lazily
- 行为:在第一个收集者出现时启动,保持活跃直到 scope 结束
- 用例:长期存在的共享数据
- 注意:可能延迟首次数据获取
started = SharingStarted.Lazily
3. SharingStarted.WhileSubscribed()
- 行为:
- 有收集者时活跃
- 最后一个收集者消失后保持一段时间(默认 0ms)
- 可配置
stopTimeoutMillis
和replayExpirationMillis
- 用例:大多数 UI 相关状态
// 保留5秒供可能的重新订阅
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000)
五、关键区别对比
特性 | stateIn | shareIn |
---|---|---|
返回类型 | StateFlow | SharedFlow |
初始值 | 必须提供 | 无要求 |
新收集者获取 | 立即获得最新 value | 获取 replay 数量的旧值 |
值访问 | 通过 .value 直接访问 | 必须通过收集获取 |
典型用途 | UI 状态管理 | 事件通知/数据广播 |
背压处理 | 总是缓存最新值 | 可配置缓冲策略 |
六、最佳实践指南
1. ViewModel 中的标准模式
class MyViewModel : ViewModel() {// 状态管理用 stateInval uiState = repository.data.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = null)// 事件处理用 shareInval events = repository.events.shareIn(scope = viewModelScope,started = SharingStarted.Lazily,replay = 1)
}
2. 合理选择 started
策略
- UI 状态:
WhileSubscribed(stopTimeoutMillis = 5000)
- 配置变更需保留:
Lazily
- 全局常驻数据:
Eagerly
3. 避免常见错误
错误1:在每次调用时创建新流
// 错误!每次调用都创建新流
fun getUser() = repository.getUserFlow().stateIn(viewModelScope, SharingStarted.Eagerly, null)// 正确:共享同一个流
private val _user = repository.getUserFlow().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val user: StateFlow<User?> = _user
错误2:忽略 replay 配置
// 可能丢失事件
.shareIn(scope, SharingStarted.Lazily, replay = 0)// 更安全的配置
.shareIn(scope, SharingStarted.Lazily, replay = 1)
七、高级应用场景
1. 结合 Room 数据库
@Dao
interface UserDao {@Query("SELECT * FROM user")fun observeUsers(): Flow<List<User>>
}// ViewModel 中
val users = userDao.observeUsers().stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = emptyList())
2. 实现自动刷新功能
val autoRefreshData = flow {while(true) {emit(repository.fetchLatest())delay(30_000) // 每30秒刷新}
}.shareIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),replay = 1
)
3. 多源数据合并
val combinedData = combine(repo1.data.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1),repo2.data.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
) { data1, data2 ->data1 + data2
}.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = emptyList()
)
八、性能优化技巧
-
合理设置 replay:
- UI 状态:
replay = 1
(确保新订阅者立即获得状态) - 事件通知:
replay = 0
(避免重复处理旧事件)
- UI 状态:
-
使用 WhileSubscribed 的过期策略:
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000,replayExpirationMillis = 60_000 // 1分钟后丢弃缓存 )
-
避免过度缓冲:
.shareIn(scope = ...,replay = 1,extraBufferCapacity = 1, // 总共缓冲2个值onBufferOverflow = BufferOverflow.DROP_OLDEST )
九、测试策略
1. 测试 StateFlow
@Test
fun testStateFlow() = runTest {val testScope = TestScope()val flow = flowOf(1, 2, 3)val stateFlow = flow.stateIn(scope = testScope,started = SharingStarted.Eagerly,initialValue = 0)assertEquals(0, stateFlow.value) // 初始值testScope.advanceUntilIdle()assertEquals(3, stateFlow.value) // 最后发射的值
}
2. 测试 SharedFlow
@Test
fun testSharedFlow() = runTest {val testScope = TestScope()val flow = flowOf("A", "B", "C")val sharedFlow = flow.shareIn(scope = testScope,started = SharingStarted.Eagerly,replay = 1)val results = mutableListOf<String>()val job = launch {sharedFlow.collect { results.add(it) }}testScope.advanceUntilIdle()assertEquals(listOf("A", "B", "C"), results)job.cancel()
}
十、总结决策树
何时使用 stateIn
?
- 需要表示当前状态(有
.value
属性) - UI 需要立即访问最新值
- 适合:页面状态、表单数据、加载状态
何时使用 shareIn
?
- 处理一次性事件
- 需要自定义缓冲策略
- 适合:Toast 消息、导航事件、广播通知
选择哪种 started
策略?
WhileSubscribed()
:大多数 UI 场景Lazily
:配置变更需保留数据Eagerly
:需要预加载的全局数据
通过本教程,应该已经掌握了 shareIn
和 stateIn
的核心用法和高级技巧。正确使用这两个操作符可以显著提升应用的性能和资源利用率。