Flow 冷流

97 阅读3分钟

冷流是什么

  • 定义没有订阅就不产生数据;每个收集者 collect 都会独立触发上游执行(“一次订阅,一次流程”)。

  • 常见代表:flow { … }、sequence 的协程版、callbackFlow / channelFlow(它们也是冷的:只有被收集时才启动回调/通道)。

冷流的关键特性

  1. 惰性:没有 collect 就不执行;取消收集会立刻停止上游。

  2. 一人一份:来一个收集者就跑一遍上游;有多个收集者就跑多遍。

  3. 背压自然:下游慢,上游会挂起等待(也可以用 buffer()/conflate()/collectLatest() 调整)。

  4. 线程切换:用 flowOn(Dispatchers.IO) 指定上游执行线程;下游收集所在线程默认是当前协程。

  5. 生命周期友好:配合 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 的最佳实践