flow-两种SharingStarted策略的区别示例
一 代码示例
viewModel.kt:// 上游数据源 - 模拟温度传感器
private val temperatureSource = flow {var temp = 20while(true) {emit(temp++)delay(1000)println("上游发射温度: $temp") // 日志观察发射}
}// WhileSubscribed - 有订阅者才收集
val temperature1 = temperatureSource.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = 0
)// Eagerly - 立即开始收集
val temperature2 = temperatureSource.stateIn(scope = viewModelScope,started = SharingStarted.Eagerly,initialValue = 0
)Fragment.kt:lifecycleScope.launch {
delay(5000)
viewModel.temperature1.collect { temp ->binding.tempText.text = "$temp°C"
}
}lifecycleScope.launch {
delay(5000)
viewModel.temperature2.collect { temp ->binding.tempText.text = "$temp°C"
}
}
二 日志
1 SharingStarted.Eagerly 订阅和不订阅 ,上游流都会执行发送
2 SharingStarted.WhileSubscribed(5000) ,不订阅
3 SharingStarted.WhileSubscribed(5000),进行订阅 ,上游流才会执行发送
3.2 当前界面不可见,上游还会执行5次(5s)才停止
二 解释
为什么需要testViewModel.temperature2执行一下,才执行 Logger.getLogger().d(“上游发射温度: $temp”)这个代码
- StateFlow 的初始化机制:
class TestViewModel: ViewModel() {// 这里只是定义了属性,但实际上并没有立即执行val temperature2 = temperatureSource.stateIn(scope = viewModelScope,started = SharingStarted.Eagerly,initialValue = 0)
}
- Kotlin 属性的延迟初始化:
- Kotlin 的属性默认是懒加载的
- 只有在首次访问属性时才会执行初始化代码
stateIn
操作符的实际执行要等到属性被首次访问
- 正确的立即收集方式:
class TestViewModel: ViewModel() {val temperature2 = temperatureSource.stateIn(...)init {// 在 init 块中触发初始化temperature2 // 这行代码会触发实际的收集}
}
- 当前代码的问题:
// Fragment中
launch {testViewModel.temperature2 // 如果没有这行,属性就不会被初始化
}
要解决这个问题,有两种方案:
// 方案1:在 ViewModel 的 init 块中触发
class TestViewModel: ViewModel() {val temperature2 = temperatureSource.stateIn(...)init {temperature2 // 确保立即初始化}
}// 方案2:使用 property delegate
class TestViewModel: ViewModel() {val temperature2 by lazy {temperatureSource.stateIn(...)}
}
所以,SharingStarted.Eagerly
的"立即"是指:一旦 StateFlow 被初始化,就会立即开始收集,而不是等待下游订阅者。但初始化本身仍需要一次属性访问来触发。
四 收集的含义
这里的"收集"指的就是 temperature1
和 temperature2
从 temperatureSource
获取值的过程。详细解释一下数据流向:
// 1. 上游数据源(冷流) - 产生数据
private val temperatureSource = flow {var temp = 20while(true) {emit(temp++) // 发射数据delay(1000)Logger.getLogger().d("上游发射温度: $temp")}
}// 2. 转换为 StateFlow(热流) - 收集并分发数据
val temperature2 = temperatureSource.stateIn(scope = viewModelScope,started = SharingStarted.Eagerly, // 立即开始收集上游数据initialValue = 0
)// 3. Fragment中的使用 - 消费数据
launch {testViewModel.temperature2.collect { temp ->Logger.getLogger().d("收到温度: $temp")}
}
数据流向图示:
temperatureSource (上游)↓ emit数据
temperature2 (StateFlow)↓ collect数据
Fragment (下游)
关键点:
temperatureSource
负责产生数据temperature2
通过stateIn
操作符收集这些数据Eagerly
模式下,一旦 StateFlow 被初始化,就开始收集上游数据- Fragment 可以通过
collect
获取这些已经被收集的数据
所以这里的"收集"包含两层含义:
- StateFlow 收集上游 Flow 的数据
- Fragment 收集 StateFlow 的数据
五
解释一下这里的收集和订阅的关系:
- 数据流的三个层次:
// 1. 源头(上游) - 产生数据
private val temperatureSource = flow {emit(temp++) // 发射数据
}// 2. 中间层 - StateFlow
val temperature2 = temperatureSource.stateIn(...) // 收集上游并暂存// 3. 终点(下游) - Fragment
launch {temperature2.collect { temp -> // 订阅并收集StateFlow的数据Logger.getLogger().d("收到温度: $temp")}
}
- collect 的两种角色:
- StateFlow 对上游的 collect:收集并存储数据
- Fragment 对 StateFlow 的 collect:订阅并消费数据
- 为什么下游也用 collect:
- Kotlin Flow API 中统一使用
collect
操作符来获取数据 - 虽然概念上是"订阅",但技术实现上仍是"收集"
collect
本质是一个挂起函数,会持续接收数据流
- 完整流程示意:
temperatureSource ----emit---→ temperature2 ----emit---→ Fragment(收集存储) (订阅收集)
所以,虽然从概念上讲 Fragment 是"订阅者",但在 Flow API 中统一使用 collect
操作符来实现数据获取,无论是中间层的 StateFlow 还是最终的消费者。