Flow的Combine使用场景以及详解

163 阅读4分钟

核心心智模型

  • combine = combineLatest:当 任意一个上游 Flow 发出新值 时,用**所有上游的“最新值”**执行一次变换并发射。
  • 首帧条件:只有当每个上游都至少发过一次后,才会出现第一个输出;某上游从未发射(或永不完成),combine 就不会产出。

典型使用场景

  1. UI 状态聚合(最常见)****

    把多个独立状态源(本地缓存、网络状态、用户操作、开关配置)合成一个 UiState。

  2. 搜索/过滤参数拼装****

    queryFlow + sortFlow + filtersFlow → SearchRequest,任一参数变化就触发新搜索或新渲染。

  3. 表单校验****

    多个输入框的合法性组合成“提交按钮是否可用”。

  4. 离线优先****

    dbFlow + networkConnectivity → 可见数据 + 是否可刷新 + 占位/错误态。

  5. 权限/设备状态拼装****

    isPermissionGranted + isGpsOn + netState → 功能可用性/提示文案。

  6. 下载/任务多源进度****

    bytesDownloaded + totalBytes + throttle开关 → 百分比/速率展示。

  7. 多路埋点/曝光合并****

    滑动位置 + 页面可见性 + 实验组 → 统一曝光流(可再采样/去重)。


API 形态与时序要点

// 2 路
fun <T1, T2, R> Flow<T1>.combine(
    other: Flow<T2>,
    transform: suspend (T1, T2) -> R
): Flow<R>

// 3 路及以上(变参/Iterable)
combine(f1, f2, f3) { a, b, c -> ... }           // 变参重载
combine(flows: Iterable<Flow<*>>) { values -> ... } // 动态数量
// 低阶版:需要手动 emit,适合异步多次发射
combineTransform(a, b) { va, vb ->
    emit(build(va, vb))
    // …也可以在这里做额外异步操作再 emit
}

时间线(示意)

A:  a1----a2----a3-------|
B:  b1--------b2-b3---|
out:     (a1,b1) (a2,b1) (a2,b2) (a3,b2) (a3,b3) …直到所有上游完成
  • 先等 A/B 都至少各一次 → 产出 (a1,b1)。
  • 后续 谁更新就用**另一边的“最新值”**再组合。

完成、异常、背压

  • 完成:combine 直到全部上游都完成才会完成;某个上游提前完成后,会继续使用它的最后一个值与其他还在发射的上游组合。
  • 异常:任一上游抛出异常 → combine 立刻失败并取消其他上游;需要兜底,把 catch { … } 放在 combine 之前
  • 背压:combine 不会“牵制”快源;快的一侧可以频繁触发输出(使用慢侧的“最新值”)。如要降频/减工,配 debounce()/sample()/conflate() 或 collectLatest。

放置与性能建议

  • 把重计算放在 combine 的 transform 内,并确保可取消(有挂起点或 yield()/isActive 检查)。
  • 首帧必出:给每个上游准备初始值(onStart { emit(initial) } 或使用有初值的 StateFlow)。
  • 降噪:在 combine 前对单路做 distinctUntilChanged(),或在合成后对整体 distinctUntilChanged()。
  • 线程:在各自上游用 flowOn(Dispatchers.IO);combine 本身不切线程。
  • 落地为 StateFlow:合成后常用 stateIn(scope, SharingStarted.WhileSubscribed(), initial) 供 UI 订阅。

实战范式

1) 聚合多源成 UiState(Compose/VM)

data class UiState(
  val list: List<Item>,
  val loading: Boolean,
  val canRefresh: Boolean,
  val error: String?
)

val uiState: StateFlow<UiState> =
    combine(
        repo.itemsFlow(),          // Flow<List<Item>>
        loader.isLoading,          // Flow<Boolean>
        net.isOnline,              // Flow<Boolean>
        repo.errorFlow()           // Flow<String?>
    ) { items, loading, online, err ->
        UiState(
            list = items,
            loading = loading,
            canRefresh = online && !loading,
            error = err
        )
    }
    .distinctUntilChanged()
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState(emptyList(), false, true, null))

2) 表单校验 + 提交可用

val canSubmit: Flow<Boolean> =
    combine(usernameFlow, emailFlow, passwordFlow) { u, e, p ->
        u.isNotBlank() && e.matches(EMAIL) && p.length >= 8
    }.distinctUntilChanged()

3) 搜索参数拼装 + 请求(最新优先)

val requestFlow =
    combine(queryFlow, sortFlow, filtersFlow) { q, s, f ->
        SearchRequest(q, s, f)
    }
    .debounce(200)                 // 可选:降频
    .distinctUntilChanged()

val resultFlow =
    requestFlow.flatMapLatest(repo::search) // 新请求取消旧请求

4) 动态数量的 Flow 组合

fun <T> combineAll(flows: List<Flow<T>>): Flow<List<T>> =
    combine(flows) { arr -> arr.map { it as T } }

与其他操作符的对比(速记)

  • zip:一一配对、短板决定产量;严格对齐用它。
  • merge:把多路交错合并,不做组合。
  • conflate:只保“最新”,无固定节拍(常放在 combine 之后降噪)。
  • debounce / sample:时间域降频;配合 combine 控制输出速率。
  • collectLatest / mapLatest:新值来就取消旧处理;和 combine 组合,做“最新优先 + 可中断”的渲染/网络。

常见易坑

  1. 未发首帧:某上游没有初值且一直没发,combine 不会产出——给初值或用 StateFlow。
  2. 误当 zip:combine 不保证“第 i 个配第 i 个”;严格对齐请用 zip。
  3. 重活放错边:把昂贵计算放在 combine 之后但又无 *Latest/降频,可能导致频繁重算。
  4. 异常处理位置:要兜住上游异常,catch {} 要放在进入 combine 之前的各自链路上。

一句话总结

combine = “谁变就用各自最新值重算一次”。 用它把多来源状态拼成单一真相(Single Source of Truth) ,配 distinctUntilChanged/debounce/*Latest/stateIn,即可得到既实时可控、对 UI 友好的状态流。