flatMapLatest:为什么它永远只认最新值?

189 阅读4分钟

一句话结论:flatMapLatest 会在新值到达时,立刻取消旧值对应的 Flow 收集,因此它天然适合"只有最新请求才有效"的场景;凡是旧结果可以丢弃的异步任务,优先考虑它。


一、核心原理:新值一到,旧任务立即作废

flatMapLatest 是一个高阶 Flow 操作符,输入是上游 Flow<T>,输出是 Flow<R>。它的行为可以拆成三步:

  1. 上游每发出一个值 T,就调用你传入的 transform: (T) -> Flow<R> 生成一个新的内部 Flow。
  2. 同时只能存在一个被收集的内部 Flow。
  3. 新内部 Flow 产生时,旧的内部 Flow 收集会被取消,从它之后发出的值不再进入下游。
// 伪代码:flatMapLatest 的语义
upstream
    .flatMapLatest { value ->
        // 每次 value 变化,都会创建新 Flow
        performAsyncWork(value)
    }
    .collect { result ->
        // 只有当前生效的内部 Flow 的结果会到达这里
    }

关键点是"取消旧收集",而不是"忽略旧值"。上游值变更的瞬间,如果旧内部 Flow 还在执行耗时操作(比如网络请求),这个操作会被取消协程;新的请求会立即启动。


二、为什么是"最新"?从状态空间说起

假设有一个搜索框,用户连续输入:"k" → "ko" → "kot" → "kotl" → "kotli"。每次输入都会触发一次搜索请求:

输入触发请求flatMapLatest 的行为
kreq-1开始收集 req-1
koreq-2取消 req-1,开始收集 req-2
kotlreq-4取消 req-3,开始收集 req-4
kotlireq-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。