核心心智模型
- combine = combineLatest:当 任意一个上游 Flow 发出新值 时,用**所有上游的“最新值”**执行一次变换并发射。
- 首帧条件:只有当每个上游都至少发过一次后,才会出现第一个输出;某上游从未发射(或永不完成),combine 就不会产出。
典型使用场景
-
UI 状态聚合(最常见)****
把多个独立状态源(本地缓存、网络状态、用户操作、开关配置)合成一个 UiState。
-
搜索/过滤参数拼装****
queryFlow + sortFlow + filtersFlow → SearchRequest,任一参数变化就触发新搜索或新渲染。
-
表单校验****
多个输入框的合法性组合成“提交按钮是否可用”。
-
离线优先****
dbFlow + networkConnectivity → 可见数据 + 是否可刷新 + 占位/错误态。
-
权限/设备状态拼装****
isPermissionGranted + isGpsOn + netState → 功能可用性/提示文案。
-
下载/任务多源进度****
bytesDownloaded + totalBytes + throttle开关 → 百分比/速率展示。
-
多路埋点/曝光合并****
滑动位置 + 页面可见性 + 实验组 → 统一曝光流(可再采样/去重)。
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 组合,做“最新优先 + 可中断”的渲染/网络。
常见易坑
- 未发首帧:某上游没有初值且一直没发,combine 不会产出——给初值或用 StateFlow。
- 误当 zip:combine 不保证“第 i 个配第 i 个”;严格对齐请用 zip。
- 重活放错边:把昂贵计算放在 combine 之后但又无 *Latest/降频,可能导致频繁重算。
- 异常处理位置:要兜住上游异常,catch {} 要放在进入 combine 之前的各自链路上。
一句话总结
combine = “谁变就用各自最新值重算一次”。 用它把多来源状态拼成单一真相(Single Source of Truth) ,配 distinctUntilChanged/debounce/*Latest/stateIn,即可得到既实时又可控、对 UI 友好的状态流。