冷流是什么
-
定义:没有订阅就不产生数据;每个收集者 collect 都会独立触发上游执行(“一次订阅,一次流程”)。
-
常见代表:flow { … }、sequence 的协程版、callbackFlow / channelFlow(它们也是冷的:只有被收集时才启动回调/通道)。
冷流的关键特性
-
惰性:没有 collect 就不执行;取消收集会立刻停止上游。
-
一人一份:来一个收集者就跑一遍上游;有多个收集者就跑多遍。
-
背压自然:下游慢,上游会挂起等待(也可以用 buffer()/conflate()/collectLatest() 调整)。
-
线程切换:用 flowOn(Dispatchers.IO) 指定上游执行线程;下游收集所在线程默认是当前协程。
-
生命周期友好:配合 repeatOnLifecycle / viewModelScope,订阅即跑、离开即停。
最小示例
val numbers: Flow<Int> = flow {
println("start expensive work") // 每次 collect 都会打印
(1..3).forEach {
delay(100)
emit(it)
}
}
lifecycleScope.launch {
numbers.collect { println("A->$it") } // 触发一遍
}
lifecycleScope.launch {
numbers.collect { println("B->$it") } // 再触发一遍(并行)
}
冷流 vs 热流(对比速记)
| 对比项 | 冷流(Cold Flow) | 热流(Hot Flow: StateFlow / SharedFlow ) |
|---|---|---|
| 生产时机 | 有收集者才生产 | 启动后就生产(与收集者无关) |
| 多订阅行为 | 每订阅一次执行一次 | 一次生产,多处消费 |
| 默认缓存 | 无 | StateFlow 永远缓存最新值;SharedFlow 可配置 replay |
| 典型用途 | 网络请求、数据库查询、一次性计算 | UI 状态、事件总线、跨界共享数据 |
| 转换方式 | shareIn / stateIn → 变热 | 用 flow {} / callbackFlow {} 重新建冷流 |
常见坑与对策
-
重复执行上游:在 UI 多处收集同一个冷流,会多次打网络/读数据库。
👉 在 ViewModel 用 stateIn/shareIn 升温并缓存。
-
背压导致卡顿:UI 收集很慢,上游被拖住。
👉 加 buffer()(允许队列)、conflate()(跳过中间值)、或 collectLatest()(只取最新)。
-
线程用错:上游重计算在主线程跑。
👉 flowOn(Dispatchers.IO);UI 层 collect 在 Main。
实战模板
1) 冷流:搜索框 + 最新请求(防抖 + 只取最新)
val queryFlow = MutableStateFlow("")
val searchResult: Flow<List<Item>> =
queryFlow
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { q ->
flow { emit(api.search(q)) } // 冷流:每次订阅都会真正请求
.flowOn(Dispatchers.IO)
}
.catch { emit(emptyList()) }
2) 冷转热:在 ViewModel 缓存为
StateFlow
val searchState: StateFlow<List<Item>> =
searchResult
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
-
WhileSubscribed(5s):无人订阅 5 秒后取消上游,避免浪费;再次订阅会重启。
3) 热流构建:UI 状态(永远有值)
data class UiState(val items: List<Item> = emptyList(), val loading: Boolean = false)
val uiState: StateFlow<UiState> =
combine(searchState, loadingFlow) { items, loading ->
UiState(items, loading)
}.stateIn(viewModelScope, SharingStarted.Eagerly, UiState())
4) 收集(Fragment / Compose)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { /* render */ }
}
}
背压与“丢/并/取最新”三板斧
-
buffer(capacity):让上游继续跑,排队等下游消费。
-
conflate():如果下游慢,保留最后一个,丢掉中间值(适合绘制/进度条)。
-
collectLatest { ... }:每来新值会取消旧处理,只处理最新(适合搜索结果渲染)。
callbackFlow / channelFlow 也是“冷”的
- 它们在 collect 时才注册回调/开启通道;取消收集会自动 awaitClose { … } 解绑。
fun locationFlow(): Flow<Location> = callbackFlow {
val cb = object : Listener { override fun onLoc(l: Location) { trySend(l) } }
client.register(cb)
awaitClose { client.unregister(cb) } // 取消时释放资源
}
何时选冷、何时选热
- 一次性/按需:网络请求、按钮触发 → 冷流。
- 持续共享的状态:UI 状态、设置项、订阅型数据 → 热流(StateFlow/SharedFlow 或冷流+stateIn/shareIn)。
归纳:冷流 = 惰性、每订阅一次独立执行;热流 = 常驻生产、可被多人同时收集。在 ViewModel 把“可能被多处收集”的冷流用 stateIn/shareIn 升温并缓存,是 Android/Compose/MVI 的最佳实践。