一句话结论:
flatMapLatest会在新值到达时,立刻取消旧值对应的 Flow 收集,因此它天然适合"只有最新请求才有效"的场景;凡是旧结果可以丢弃的异步任务,优先考虑它。
一、核心原理:新值一到,旧任务立即作废
flatMapLatest 是一个高阶 Flow 操作符,输入是上游 Flow<T>,输出是 Flow<R>。它的行为可以拆成三步:
- 上游每发出一个值
T,就调用你传入的transform: (T) -> Flow<R>生成一个新的内部 Flow。 - 同时只能存在一个被收集的内部 Flow。
- 新内部 Flow 产生时,旧的内部 Flow 收集会被取消,从它之后发出的值不再进入下游。
// 伪代码:flatMapLatest 的语义
upstream
.flatMapLatest { value ->
// 每次 value 变化,都会创建新 Flow
performAsyncWork(value)
}
.collect { result ->
// 只有当前生效的内部 Flow 的结果会到达这里
}
关键点是"取消旧收集",而不是"忽略旧值"。上游值变更的瞬间,如果旧内部 Flow 还在执行耗时操作(比如网络请求),这个操作会被取消协程;新的请求会立即启动。
二、为什么是"最新"?从状态空间说起
假设有一个搜索框,用户连续输入:"k" → "ko" → "kot" → "kotl" → "kotli"。每次输入都会触发一次搜索请求:
| 输入 | 触发请求 | flatMapLatest 的行为 |
|---|---|---|
| k | req-1 | 开始收集 req-1 |
| ko | req-2 | 取消 req-1,开始收集 req-2 |
| kotl | req-4 | 取消 req-3,开始收集 req-4 |
| kotli | req-5 | 取消 req-4,开始收集 req-5 |
最终只有 req-5 的结果会被展示。
与兄弟操作符的对比
// 1) flatMapConcat:串行,先等前一个完全收集完,再处理下一个
flowOf("a", "b", "c")
.flatMapConcat { request(it) } // a -> b -> c 顺序执行
// 2) flatMapMerge:并发,所有内部 Flow 同时跑,结果谁先完成谁先出
flowOf("a", "b", "c")
.flatMapMerge { request(it) } // 可能 c 的结果比 a 先到
// 3) flatMapLatest:只保留最新,旧请求被取消
flowOf("a", "b", "c")
.flatMapLatest { request(it) } // 只有 c 的结果会到下游
flatMapLatest 把"选择结果"的逻辑从下游搬到了上游:不是下游去判断哪个结果更新,而是操作符直接帮你丢弃旧的。
三、内部实现关键:协程作用域 + Job 替换
Kotlin Flow 中,flatMapLatest 依赖协程的结构化并发。
// 简化版核心逻辑示意
fun <T, R> Flow<T>.flatMapLatest(transform: suspend (T) -> Flow<R>): Flow<R> =
channelFlow {
var currentJob: Job? = null
collect { value ->
currentJob?.cancelAndJoin() // 取消上一个内部 Flow 的收集
currentJob = launch {
transform(value).collect { innerValue ->
send(innerValue)
}
}
}
}
currentJob持有当前正在收集的内部 Flow 的协程任务。- 新值到达时,
cancelAndJoin()取消旧任务,保证同一时刻只有最新值在跑。 - 被取消的协程会抛出
CancellationException,这是正常取消,不会污染错误处理。
注意:如果你的异步任务内部没正确响应取消(比如阻塞 IO 没使用
isActive检查),旧任务可能还会继续执行,只是结果会被丢弃。所以写flatMapLatest时,内部 Flow 必须是"可取消的"。
四、统一示例:SearchViewModel
用同一个 SearchViewModel 展示实际用法。
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
// 输入变化时,旧搜索被取消,只展示最新结果
val searchResults: Flow<List<Repo>> = _query
.debounce(300) // 防抖,减少无意义请求
.filter { it.isNotBlank() } // 忽略空输入
.flatMapLatest { q -> // 只有最新查询有效
githubApi.searchRepositories(q)
.catch { emit(emptyList()) } // 错误兜底
}
fun onQueryChanged(text: String) {
_query.value = text
}
}
.debounce() 与 .flatMapLatest() 通常成对出现:前者减少触发频率,后者保证只保留最后一个有效请求。
五、适用场景
旧结果无意义时,优先考虑 flatMapLatest。
| 场景 | 核心逻辑 |
|---|---|
| 搜索框实时查询 | queryFlow.debounce(300).flatMapLatest { api.search(it) } |
| 筛选条件频繁切换 | filterFlow.flatMapLatest { repo.fetch(it) } |
| 地图/定位实时更新 | regionFlow.flatMapLatest { api.fetchPois(it) } |
| 表单联动 | provinceFlow.flatMapLatest { api.getCities(it) } |
| 替代手动 Job 管理 | keywordFlow.flatMapLatest { api.search(it) } 替代 searchJob?.cancel() |
六、不适合的场景
| 场景 | 原因 | 替代方案 |
|---|---|---|
| 需要累加/聚合结果 | 旧值会被丢弃 | flatMapMerge / combine |
| 支付、下单等不可逆操作 | 取消可能导致状态不一致 | 单次请求 + 结果缓存 |
| 需要严格顺序保证 | 不保证顺序 | flatMapConcat |
| 旧任务结果仍有意义 | 会丢数据 | flatMapMerge |
七、完整可运行示例
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
object FakeSearchApi {
suspend fun search(query: String): List<String> {
delay(500) // 模拟网络耗时
return listOf("$query-结果1", "$query-结果2")
}
}
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
val searchResults: Flow<List<String>> = _query
.debounce(200)
.filter { it.isNotBlank() }
.flatMapLatest { q ->
flow {
println("开始搜索: $q")
emit(FakeSearchApi.search(q))
}
}
.catch { emit(emptyList()) }
fun onQueryChanged(text: String) {
_query.value = text
}
}
fun main() = runBlocking {
val viewModel = SearchViewModel()
val job = launch {
viewModel.searchResults.collect { result ->
println("收到结果: $result")
}
}
viewModel.onQueryChanged("k")
delay(100)
viewModel.onQueryChanged("ko")
delay(100)
viewModel.onQueryChanged("kot")
delay(2000)
job.cancel()
}
运行后只有 "kot" 的结果到达 collect,"k" 和 "ko" 的请求会被取消。
八、总结
flatMapLatest:上游发新值 → 取消旧内部 Flow → 收集最新内部 Flow。- 适合:旧结果无意义的异步任务(搜索、筛选、地图、表单联动)。
- 不适合:需要顺序、累加、不可逆操作。
- 判断标准:业务上旧结果是否可被丢弃。可以,就用
flatMapLatest;不行,用flatMapMerge或flatMapConcat。