SharedFlow vs StateFlow 与 冷流 vs 热流:深入解析与示例
SharedFlow(replay = 1) vs StateFlow
虽然 SharedFlow(replay = 1)
和 StateFlow
看起来很相似,但它们有重要区别:
特性 | SharedFlow(replay = 1) | StateFlow |
---|---|---|
初始值 | 不需要初始值 | 必须提供初始值 |
值相等性 | 总是发射所有值 | 跳过与当前值相等的值 |
空值处理 | 可以发射 null | 初始值不能为 null |
重放机制 | 明确指定重放数量 | 固定重放最新值 |
值更新 | 所有值都会触发收集 | 只有不同值才会触发 |
适用场景 | 事件、通知 | 状态管理 |
转换关系 | 更基础的原语 | 本质上是 SharedFlow(replay=1) 的特殊实现 |
代码示例对比
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
// SharedFlow(replay=1) 示例
val sharedFlow = MutableSharedFlow<Int>(replay = 1)
launch {
repeat(5) { i ->
sharedFlow.emit(i)
delay(100)
}
}
delay(250)
println("SharedFlow 收集:")
sharedFlow.collect { println(" 收集到: $it") }
// StateFlow 示例
val stateFlow = MutableStateFlow(-1) // 必须初始值
launch {
repeat(5) { i ->
stateFlow.value = i
delay(100)
}
}
delay(250)
println("StateFlow 收集:")
stateFlow.collect { println(" 收集到: $it") }
}
输出:
SharedFlow 收集:
收集到: 2 // 最近发射的值
收集到: 3
收集到: 4
StateFlow 收集:
收集到: -1 // 初始值
收集到: 2 // 最近状态值
收集到: 3
收集到: 4
关键区别分析
- 初始值处理:
StateFlow
强制要求初始值,收集者首先收到初始值SharedFlow
没有初始值,新收集者只收到重放的值
- 值相等性检查:
// SharedFlow - 总是发射
sharedFlow.emit(1) // 收集者收到1
sharedFlow.emit(1) // 收集者再次收到1
// StateFlow - 跳过相同值
stateFlow.value = 1 // 收集者收到1
stateFlow.value = 1 // 无变化,不触发收集
- null 处理:
// SharedFlow 支持 null
val shared = MutableSharedFlow<String?>(replay=1)
shared.emit(null) // 有效
// StateFlow 初始值不能为 null
val state = MutableStateFlow(null) // 编译错误!
冷流 vs 热流:概念与示例
冷流 (Cold Flow)
定义:
- 每个收集者触发独立的流执行
- 类似 DVD – 每次播放从头开始
- 数据生产是惰性的(有收集者才开始)
- 适合一次性数据操作
特性:
- 每个收集者有独立数据序列
- 无状态,不保存历史
- 资源消耗与收集者数量相关
- 典型实现:
flow { }
构建器
示例场景:
- 网络请求
- 数据库查询
- 文件读取
代码示例:
val coldFlow = flow {
println("开始执行")
emit(1)
emit(2)
emit(3)
}
fun main() = runBlocking {
// 第一次收集
coldFlow.collect { println("收集者A: $it") }
// 第二次收集(重新执行)
coldFlow.collect { println("收集者B: $it") }
}
输出:
热流 (Hot Flow)
定义:
- 数据发射独立于收集者存在
- 类似电视直播 – 随时加入观看
- 数据生产立即开始
- 多个收集者共享相同数据流
特性:
- 收集者共享同一数据序列
- 有状态,可保留历史数据
- 资源消耗与收集者数量无关
- 典型实现:
SharedFlow
,StateFlow
示例场景:
- 用户位置更新
- 实时股价推送
- 应用状态管理
- 全局事件总线
代码示例:
val hotFlow = MutableSharedFlow<Int>(replay = 1)
fun main() = runBlocking {
// 开始发射数据(无收集者)
launch {
repeat(5) { i ->
hotFlow.emit(i)
delay(100)
}
}
delay(250) // 等待发射部分数据
// 第一个收集者
launch {
hotFlow.collect { println("收集者A: $it") }
}
delay(200)
// 第二个收集者
launch {
hotFlow.collect { println("收集者B: $it") }
}
}
输出:
冷流 vs 热流对比表
特性 | 冷流 | 热流 |
---|---|---|
数据生产时机 | 收集时开始 | 创建时开始 |
数据共享 | 每个收集者独立 | 所有收集者共享 |
历史数据 | 无 | 可保留 |
资源消耗 | 与收集者数量成正比 | 固定 |
状态保持 | 无状态 | 有状态 |
典型用例 | 一次性操作 | 持续状态/事件 |
创建方式 | flow { } | MutableSharedFlow |
转换方法 | 使用 shareIn | 使用 onStart |
实际应用场景示例
冷流用例:API 请求
fun fetchUserData(userId: String): Flow<User> = flow {
// 每次收集触发新请求
val response = apiService.getUser(userId)
emit(response.toUser())
}
// 使用
viewModelScope.launch {
fetchUserData("123").collect { user ->
updateUI(user)
}
}
热流用例:实时位置追踪
class LocationService {
private val _locations = MutableSharedFlow<Location>(replay = 1)
val locations: SharedFlow<Location> = _locations
init {
startLocationUpdates { location ->
_locations.tryEmit(location)
}
}
}
// 多个UI组件使用
class MapFragment {
locationService.locations.collect { /* 更新地图 */ }
}
class StatusBar {
locationService.locations.collect { /* 显示位置状态 */ }
}
混合用例:缓存策略
fun getData(): Flow<Data> {
// 冷流:首次从网络加载
val networkFlow = flow {
val data = fetchFromNetwork()
emit(data)
}
// 热流:缓存最新值
return networkFlow.shareIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
replay = 1
)
}
如何选择
- 选择冷流当:
- 需要按需触发操作(如按钮点击)
- 数据是短暂的一次性操作
- 不同收集者需要独立数据
- 选择热流当:
- 需要持续观察变化(如数据库)
- 多个组件需要共享相同状态
- 需要保留历史数据
- 事件驱动架构(如全局通知)
- 选择 SharedFlow 当:
- 需要事件总线
- 需要自定义重放策略
- 需要发射 null 值
- 需要所有值都触发收集
- 选择 StateFlow 当:
- 管理UI状态
- 需要初始值
- 自动跳过重复值
- 简单状态管理场景
理解这些概念的区别对于设计高效、响应式的Kotlin应用程序至关重要。根据具体需求选择合适的流类型,可以显著提升应用性能和代码可维护性。