flow的终端操作、取消协作

75 阅读3分钟

一、终端操作(Terminal operators)

一旦调用就启动收集,并决定这条 Flow 的“生命周期结束点”。

1) 收集类

  • collect { ... }:顺序处理每个值(会对上游施加背压)。
  • collectLatest { ... }:来新值就取消上一次处理块,只处理最新值。
  • launchIn(scope):把 Flow 启动到 scope 中,返回 Job(常与 onEach 搭配)。相当于“边返回边运行”。
flow.onEach { render(it) }.launchIn(viewModelScope)  // 启动并返回 Job

2) 取值/短路类(会在满足条件后取消上游)

  • first() / firstOrNull():取第一个值后结束。

  • single() / singleOrNull():要求仅有一个元素。

  • last():收集到末尾的最后一个值。

  • toList() / toSet() / toCollection(coll):收集成集合。

  • reduce { acc, v -> ... } / fold(init) { acc, v -> ... }

  • count() / any {} / all {} / none {}

take(n) 属于中间操作,但它会在拿够 n 个后主动取消上游

3) “启动型共享”操作(本质是中间,但会启动上游)

  • stateIn(scope, started, initial):把冷流升为 StateFlow(有“当前值”)。

  • shareIn(scope, started, replay):把冷流升为 SharedFlow(广播式)。

它们返回新的 Flow(热流),但由于会在给定 scope 内启动收集,表现出“终端”的效果。


二、取消协作(Cooperative cancellation)

协程的取消是协作式的:通过抛出 CancellationException 让各层尽快结束。Flow/挂起函数都应对取消敏感

1) 取消如何传播

  • 下游取消会向上游传播:first() / take(n) / 手动 job.cancel()。
  • collectLatest:新值到来会取消之前那一次收集块(不是上游),随后立即处理最新值。
  • launchIn(scope):取消返回的 Job 或取消 scope,都会停止这条 Flow。
val job = flow.onEach { ... }.launchIn(scope)
// ...
job.cancel() // 立即停止收集与上游

2) 让代码“可被取消”的要点

  • 只用可挂起的 API:例如 delay、withContext(Dispatchers.IO)、Retrofit/Room 的挂起函数等。
  • CPU 密集循环:定期 yield() / ensureActive() / 检查 isActive。
  • 阻塞 IO:放到 Dispatchers.IO;若库支持取消(如 OkHttp Call.cancel()),用 suspendCancellableCoroutine 绑定取消。
// CPU 密集:定期让出
while (...) {
    ensureActive()    // 或 yield()
    heavyStep()
}

3) 常用取消相关算子与回调

  • onCompletion { cause -> ... }:正常完成 cause==null;取消/异常时 cause 非空(注意区分)。
  • cancellable():让基于 asFlow() 的迭代在每次发射前检查取消(对长链表/昂贵迭代很有用)。
  • timeout(ms) / withTimeout(ms):超时自动取消并抛出 TimeoutCancellationException。
flowOf(1,2,3).onCompletion { cause ->
    if (cause is CancellationException) log("cancelled")
}

4) 处理异常时

不要吞掉取消

flow.catch { e ->
    if (e is CancellationException) throw e   // 继续向上抛
    emit(fallback())
}

5) 生命周期集成(Android)

  • Fragment:repeatOnLifecycle(STARTED) { flow.collect { ... } } 自动在不可见时取消、可见时重启
  • Compose:collectAsStateWithLifecycle()。
  • 共享上游但无订阅应停:WhileSubscribed(5_000)。
val ui: StateFlow<UiState> =
    repo.dataFlow
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState())

三、典型场景模板

1) 搜索:只取最新请求(老请求自动取消)

val query = MutableStateFlow("")

val resultFlow =
    query.debounce(300)
         .distinctUntilChanged()
         .flatMapLatest { q -> flow { emit(api.search(q)) }.flowOn(Dispatchers.IO) }

lifecycleScope.launch {
    resultFlow.collectLatest { render(it) }  // 新查询到来,旧渲染取消
}

2) 启动到作用域 & 手动取消

val job = flow.onEach { render(it) }
              .launchIn(viewLifecycleOwner.lifecycleScope)
// 需要时:
job.cancel()

3) “拿够就停”:take/first

val top5 = repo.itemsFlow
    .take(5)              // 拿够 5 个会主动取消上游
    .toList()

4) 清理与不可取消区

try {
    flow.collect { ... }
} finally {
    withContext(NonCancellable) {
        // 必须完成的清理(关闭文件/释放句柄)
    }
}

四、容易踩的点(速记)

  1. 以为 collectLatest 不会背压上游****

    • 如果上游没有缓冲,仍可能被挂起。→ 加 buffer() 或为 SharedFlow 配 extraBufferCapacity/DROP_*。
  2. 在 catch 里吞掉 CancellationException****

    • 一定要 if (e is CancellationException) throw e。
  3. 把一次性事件放进 StateFlow****

    • 新订阅立刻收到旧事件。→ 用 SharedFlow(replay=0)。
  4. 忘记停“共享上游”****

    • stateIn/shareIn 要用 WhileSubscribed,或手动取消 scope。

小结

  • 终端操作:collect/collectLatest/launchIn、各类取值/聚合(first/single/last/toList/reduce/fold),以及会启动上游的 stateIn/shareIn。

  • 取消协作:让上游/处理块可取消(挂起、yield/ensureActive、绑定底层取消);正确使用 onCompletion、不吞取消异常;在 Android 用 repeatOnLifecycle/collectAsStateWithLifecycle 管理订阅生命周期。

需要我把你某段 Flow 代码“加上正确的终端操作与取消处理”(比如防抖、只取最新、WithSubscribed、异常兜底)的,贴出来我帮你改成最佳实践版本。