热流(Hot Flow)和冷流(Cold Flow)是 Kotlin 协程中 Flow 的两种不同行为模式,它们在数据发射、订阅机制和使用场景上有显著区别。以下是详细解析:
1. 冷流(Cold Flow)
定义
冷流是按需触发的:只有被收集(
collect)时才会开始发射数据。每个收集者独立触发流:每个新的订阅者都会触发一次完整的数据流执行。
类似“单次数据流”:每次订阅都会重新执行流的逻辑(如网络请求、数据库查询)。
特点
数据独立性:每个收集者获取的数据流是独立的。
生命周期绑定收集者:流的执行随收集者的协程作用域取消而终止。
代表实现
通过
flow { ... }构建的标准Flow。由
asFlow()、flowOf()等创建的流。
使用场景
一次性数据操作:如网络请求、数据库查询。
需要按需触发的任务:例如用户手动刷新数据。
代码示例
// 冷流示例:每次收集都会重新执行
fun fetchDataCold(): Flow<String> = flow {
println("冷流:开始执行网络请求")
delay(1000) // 模拟耗时操作
emit("数据结果")
}
// 收集两次,触发两次独立的流执行
viewModelScope.launch {
fetchDataCold().collect { println("收集结果1: $it") } // 输出结果
fetchDataCold().collect { println("收集结果2: $it") } // 再次执行并输出
}
输出:
冷流:开始执行网络请求
收集结果1: 数据结果
冷流:开始执行网络请求
收集结果2: 数据结果
2. 热流(Hot Flow)
定义
热流是主动触发的:无论是否有收集者订阅,数据都会持续发射。
多订阅者共享数据:所有收集者共享同一个数据流,后续订阅者可能错过之前的数据。
类似“广播”:数据发射独立于收集者的生命周期。
特点
数据共享性:多个收集者共享同一数据源。
生命周期独立:流可能持续运行,需要手动管理资源(如取消)。
代表实现
StateFlow:始终保存最新状态,新订阅者立即获得当前值。SharedFlow:可配置历史数据缓存(如replay),支持多订阅者。通过
Channel转换的流:如consumeAsFlow()。
使用场景
实时状态管理:如 UI 状态、用户位置更新。
事件广播:如全局通知、实时消息推送。
代码示例
// 热流示例:SharedFlow
private val _sharedData = MutableSharedFlow<String>(replay = 1) // 缓存最近1个值
val sharedData: SharedFlow<String> = _sharedData
// 主动发射数据(不依赖收集者)
fun startEmitting() {
viewModelScope.launch {
repeat(3) {
delay(1000)
_sharedData.emit("数据$it")
println("热流:发射数据$it")
}
}
}
// 收集示例
viewModelScope.launch {
// 延迟订阅,错过前两次发射
delay(2500)
sharedData.collect { println("收集结果: $it") }
}
输出:
热流:发射数据0
热流:发射数据1
热流:发射数据2
收集结果: 数据1 // 因 replay=1,收到最后一次缓存的 "数据1"
收集结果: 数据2 // 后续实时数据
3. 核心区别对比
| 特性 | 冷流(Cold Flow) | 热流(Hot Flow) |
|---|---|---|
| 触发时机 | 按需触发(收集时启动) | 主动触发(独立于收集者) |
| 数据共享性 | 每个收集者独立执行流 | 多收集者共享同一数据源 |
| 历史数据 | 每次订阅从头开始 | 可配置缓存(如 replay) |
| 资源管理 | 自动随收集者取消释放 | 需手动管理(如取消协程) |
| 典型使用场景 | 网络请求、数据库查询 | 实时状态、事件广播 |
| 代表实现 |
flow { ... }、asFlow()
|
StateFlow、SharedFlow
|
4. 使用场景代码对比
冷流场景:每次收集触发独立请求
// 冷流:每次收集重新执行数据库查询
fun getUserByIdCold(id: Int): Flow<User> = flow {
val user = database.queryUser(id) // 每次收集触发查询
emit(user)
}
// 两个界面分别收集同一流
viewModelScope.launch {
getUserByIdCold(1).collect { println("界面1: $it") }
}
viewModelScope.launch {
getUserByIdCold(1).collect { println("界面2: $it") }
}
// 输出两次数据库查询日志
热流场景:共享实时状态
// 热流:StateFlow 实时更新用户状态
private val _userState = MutableStateFlow<User?>(null)
val userState: StateFlow<User?> = _userState.asStateFlow()
// 更新状态
fun updateUser(user: User) {
_userState.value = user
}
// 多个界面观察同一状态
viewModelScope.launch {
userState.collect { println("界面1: $it") }
}
viewModelScope.launch {
userState.collect { println("界面2: $it") }
}
// 所有界面实时收到相同状态更新
5. 总结
冷流:适用于按需触发、数据独立的场景(如网络请求),每次订阅重新执行逻辑。
热流:适用于主动推送、数据共享的场景(如 UI 状态管理),多个订阅者共享同一数据源。
根据业务需求选择合适的数据流类型,可以显著提升代码的效率和可维护性。
热流与冷流的深入解析
1. 热流与冷流的转换技巧
在实际开发中,常需要将冷流转换为热流,以共享数据源或保持状态持久化。以下是常见的转换方法:
使用 stateIn 将冷流转为 StateFlow
class MyViewModel(repository: MyRepository) : ViewModel() {
// 将冷流转换为热流 StateFlow
val data: StateFlow<Result<String>> = repository.fetchDataCold()
.map { Result.Success(it) }
.catch { emit(Result.Error(it)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), // 5秒无订阅者后停止
initialValue = Result.Loading
)
}
// Repository 返回冷流
class MyRepository {
fun fetchDataCold(): Flow<String> = flow {
emit("数据")
delay(1000)
}
}
作用:确保多个UI组件订阅同一数据源时,不会重复触发网络请求。
-
参数说明:
-
started:控制流的共享策略。WhileSubscribed表示无订阅者时自动取消上游流,避免资源浪费。
-
使用 shareIn 转为 SharedFlow
val hotFlow: SharedFlow<String> = coldFlow
.shareIn(
scope = viewModelScope,
started = SharingStarted.Eagerly, // 立即启动,不等待订阅者
replay = 1 // 新订阅者收到最近1个值
)
2. 如何选择热流与冷流?
根据场景需求做出决策:
-
选择冷流的情况:
数据需按需触发:如点击按钮后加载数据。
独立数据副本:每个订阅者需要完整的数据流(如日志记录)。
资源敏感操作:如大文件下载,确保无订阅时立即释放资源。
-
选择热流的情况:
状态持久化:如用户登录状态,需跨界面共享。
实时事件推送:如WebSocket消息、传感器数据。
避免重复计算:多个订阅者共享同一计算结果。
3. SharedFlow vs StateFlow:核心区别
两者均为热流,但适用场景不同:
| 特性 | SharedFlow | StateFlow |
|---|---|---|
| 数据缓存 | 可配置 replay(缓存多个历史值) |
仅缓存最新值(等效 replay=1) |
| 初始值 | 不需要初始值 | 必须提供初始值 |
| 适用场景 | 事件通知(如按钮点击) | 状态管理(如UI控件的显示/隐藏) |
| 订阅者接收数据 | 新订阅者收到 replay 缓存的历史值 |
立即收到最新值 |
SharedFlow 示例:事件通知
// ViewModel 中定义事件流
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events
fun onButtonClick() {
viewModelScope.launch {
_events.emit(Event.ShowToast("点击成功"))
}
}
// Activity 收集事件
lifecycleScope.launch {
viewModel.events.collect { event ->
when (event) {
is Event.ShowToast -> showToast(event.message)
}
}
}
StateFlow 示例:UI状态管理
private val _loadingState = MutableStateFlow(false)
val loadingState: StateFlow<Boolean> = _loadingState.asStateFlow()
fun loadData() {
viewModelScope.launch {
_loadingState.value = true
// 模拟加载数据
delay(1000)
_loadingState.value = false
}
}
4. 常见陷阱与解决方案
陷阱1:冷流重复触发网络请求
-
问题代码:
// 每次 collect 都会触发新请求 fun loadData() = flow { fetchDataFromNetwork() } -
修复方案:
使用
stateIn或shareIn转为热流,确保数据共享。
陷阱2:未正确处理生命周期导致内存泄漏
-
错误示例:
// 在 Activity 中直接收集,未绑定生命周期 viewModel.data.collect { ... } // 可能导致后台继续收集 -
正确做法:
使用
repeatOnLifecycle确保只在活跃状态收集:lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.data.collect { ... } } }
陷阱3:SharedFlow 事件被多次处理
问题:多个订阅者导致事件重复消费。
解决:使用
SharedFlow(extraBufferCapacity=0)或单订阅者模式。
5. 完整场景代码示例
场景:冷流转热流 + 状态管理
// Repository 提供冷流
class DataRepository {
fun fetchData(): Flow<String> = flow {
delay(1000)
emit("新数据")
}
}
// ViewModel 转换为热流
class MyViewModel(repository: DataRepository) : ViewModel() {
private val _data = repository.fetchData()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = "初始值"
)
val data: StateFlow<String> = _data
}
// Activity 安全收集
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { data ->
updateUI(data)
}
}
}
}
}
总结
冷流:轻量级、按需触发,适合独立数据操作。
热流:持久化、共享数据源,适合状态管理和实时事件。
-
关键技巧:
使用
stateIn/shareIn优化冷流为热流。通过
repeatOnLifecycle确保安全收集。根据事件(SharedFlow)或状态(StateFlow)选择合适的热流类型。
掌握两者的区别与应用场景,能显著提升代码的可维护性和性能表现。