Kotlin Flow之合并

2,101 阅读26分钟

merge

merge函数用于将多个流合并成一个流,所有输入流中发出的数据将按照它们发出的顺序不加区分地混合在一起,依次被发送到下游。

示例:


import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val flow1 = flow {
        emit(1)
        kotlinx.coroutines.delay(100)
        emit(3)
    }

    val flow2 = flow {
        emit(2)
        kotlinx.coroutines.delay(50)
        emit(4)
    }

    // 将两个flow合并为一个flow
    val mergedFlow = merge(flow1, flow2)

    mergedFlow.collect { value ->
        println(value)
    }
}
//  输出:
1
2
3
4

解释

在上面的例子中,flow1flow2都在发出一些数据,使用merge函数将它们合并成一个流mergedFlow。最终输出显示的是两个流的数据交错在一起。

merge还可以合并flow的flow,例如这样:

val flowList = listOf(flow1, flow2)
val mergedFlowFromList = flowList.merge()

这样合并之后的flow会按照顾flow1和flow2中的元素发送数据依次把数据发送出来,效果和merge一样。

merge的合并是可以理解为真的把多个flow内部的执行代码完全合并到一起了,所以他们的顺序与单个flow的执行可能就不一样了。因此它具有以下几个特性。

  • 并发性merge函数允许多个Flow同时发出数据。这意味着它们的数据流是并行处理的,而不是顺序处理的。因此,merge不会等待某个流完成或发出数据后再处理其他流的数据。

  • 无序性:由于多个Flow是并发执行的,所以最终合并后的流中数据的顺序可能与单个流的顺序不一致。数据会根据各个Flow的发出速度交错在一起。这意味着merge的输出是无序的,反映了输入流发出数据的顺序。

  • 实时性merge函数能够立即处理任何输入流发出的数据,并将其传递给下游。这使得merge非常适合需要处理实时数据的场景。

适用场景

  • 当需要从多个数据源(如网络请求、传感器数据、用户输入等)收集数据,并且这些数据源可能会同时发出数据时,merge是一个理想的选择。
  • 适用于处理实时数据流的应用程序,例如实时聊天、股票行情监控或多路视频流合成。

flattenConcat

Flow中的flattenConcat函数是一个用来处理嵌套流(也叫“流的流”)的操作符。它将嵌套的多个流按顺序一个接一个地连接起来,形成一个连续的流,最终输出为一个平展后的流,这是因为当一个flow发出元素之后,直接使用挂起协程的形式,把这个生产元素的flow给暂停了,暂停的时候去收集该元素发送的数据。当第一个元素被处理完成后,才会继续进行下一个元素的生产。挨个处理每一个flow以及处理每一个flow里边发送的数据。

主要特点:

  1. 顺序执行flattenConcat按顺序处理每个嵌套的流。当一个流中的所有元素都被发出后,才会开始处理下一个流。这与merge并行处理不同,flattenConcat保证每个流在上一个流完成后再开始。
  2. 无并发性:与merge不同,flattenConcat是按顺序逐一处理的,不会并发执行每个流。这意味着如果一个流很慢或者有大量元素,后面的流会被阻塞,直到前一个流完成。
  3. 无序合并:虽然每个子流内部的元素顺序是保留的,但不同子流之间的元素顺序是按顺序依次发出的。
  4. 常见使用场景flattenConcat适合用于需要按照严格顺序处理多个流的场景。例如,处理多个API请求的响应,其中每个请求的结果必须在下一个请求之前完全处理完毕。

示例代码:

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val flow1 = flow {
        emit(1)
        kotlinx.coroutines.delay(100)
        emit(2)
    }

    val flow2 = flow {
        emit(3)
        kotlinx.coroutines.delay(50)
        emit(4)
    }

    val combinedFlow = listOf(flow1, flow2).asFlow().flattenConcat()

    combinedFlow.collect { value ->
        println(value)
    }
}
// 输出
1
2
3
4

解释:

在这个示例中,flow1flow2被分别创建并存储在一个列表中。asFlow()将这个列表转换为一个Flow,其中每个元素都是一个FlowflattenConcat会顺序地处理每个Flow,首先收集flow1的所有元素,然后再收集flow2的元素。因此,输出会是1、2、3、4,顺序地发出每个流的所有元素。

适用场景:

  • 严格顺序处理:当你需要确保多个流的数据按照严格的顺序进行处理时,flattenConcat是理想的选择。例如,依赖前一个流的结果来决定后一个流的操作。
  • 避免并发:在某些情况下,并发执行多个流可能会导致不期望的结果或复杂性增加。flattenConcat通过串行处理流,简化了这种场景的处理。

flattenMerge

flattenMerge 是 Kotlin Flow 中的一个操作符,用于将多个 Flow 合并成一个 Flow,并且所有的 Flow 都是并发地发出数据。与 flattenConcat 不同,flattenMerge 允许流之间的数据发出顺序无关紧要。

主要特点

  1. 并发处理flattenMerge 会并发处理所有的流。当有多个嵌套的流时,这些流会同时开始发出数据,而不需要等待其他流完成。
  2. 无序输出:由于并发处理,flattenMerge 不保证输出数据的顺序。数据的发出顺序取决于每个流内部的处理速度。
  3. 适用场景flattenMerge 适合在不关心顺序的情况下,将多个流的数据快速合并处理的场景。例如,当有多个数据源并且需要同时获取数据时,flattenMerge 是一个很好的选择。

示例:

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val flow1 = flow {
        emit(1)
        kotlinx.coroutines.delay(100)
        emit(2)
    }

    val flow2 = flow {
        emit(3)
        kotlinx.coroutines.delay(50)
        emit(4)
    }

    // 创建一个包含多个 Flow 的流
    val flowOfFlows = flowOf(flow1, flow2)

    // 使用 flattenMerge 合并这些 Flow
    flowOfFlows.flattenMerge().collect { value ->
        println(value)
    }
}
// 输出:
1
3
4
2

解释: 在这个例子中,flow1flow2 被创建为两个单独的流。flowOfFlows 是一个包含这些流的流。flattenMerge 允许 flow1flow2 并发执行,因此输出的顺序不固定。最终的输出顺序取决于各个流中的 emit 语句执行的速度。

使用场景

  • 并发数据源:例如,当从多个 API 请求中获取数据,且不关心数据返回的顺序时,可以使用 flattenMerge 将所有请求的结果合并到一个流中。
  • 事件处理:当处理来自多个事件源的数据时,如果不关心事件处理的顺序,可以使用 flattenMerge 并发处理这些事件。

flatMapConcat

flatMapConcat 是一个用于处理嵌套流(Flow of Flow)的操作符。它可以将一个流中的元素转换为多个流,然后将这些流顺序连接成一个单一的流进行收集和处理,它类似于map + faltConcat的结合体。

flatMapConcat 的主要特点:

  1. 顺序连接

    • flatMapConcat 会顺序地处理每个流。当第一个流中的所有元素都被处理完毕后,才会开始处理下一个流。这与 flatMapMerge 并发处理不同,flatMapConcat 保证每个流的处理是顺序进行的。
  2. 无并发性

    • 在使用 flatMapConcat 时,所有的流都是按顺序依次执行的,不会出现并发执行的情况。这意味着如果一个流执行时间较长,那么下一个流会等待前一个流完成后才开始执行。
  3. 常见使用场景

    • 适用于需要保持严格的顺序性,并且需要将每个元素映射为一个流的场景。例如,一个操作需要按顺序执行多个 API 请求,并且每个请求的执行依赖于前一个请求的结果。

示例:

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val numbersFlow = flowOf(1, 2, 3)

    val resultFlow = numbersFlow.flatMapConcat { number ->
        flow {
            emit("$number: First")
            kotlinx.coroutines.delay(100)
            emit("$number: Second")
        }
    }

    resultFlow.collect { value ->
        println(value)
    }
}
// 输出:
1: First
1: Second
2: First
2: Second
3: First
3: Second

解释:

在这个示例中,numbersFlow 是一个简单的流,发出 1, 2, 3 三个整数。flatMapConcat 操作符将每个数字映射为一个新的流,该流依次发出两个字符串。由于 flatMapConcat 的顺序性,处理完第一个数字对应的流后,才会开始处理下一个数字对应的流。

适用场景:

  • 严格顺序处理:当需要按照严格的顺序处理每个元素,并且将每个元素映射为一个流时,flatMapConcat 是非常合适的选择。
  • 避免并发:在某些情况下,并发处理可能会导致逻辑复杂度增加或者不符合业务需求,flatMapConcat 通过串行处理流,简化了这些场景的处理。

flatMapMerge

相当于map + flatMerge,将一个流中的元素转换为多个流,然后将这些流合并成一个单一的流进行收集和处理。与 flatMapConcat 不同,flatMapMerge 会并发处理每个流,因此它在处理速度和效率上更高,但顺序性无法保证。

flatMapMerge 的主要特点:

  1. 并发处理

    • flatMapMerge 会并发地执行每个流的转换和处理。这意味着所有生成的流会同时启动,数据按最快的流到达顺序合并输出。
  2. 无序输出

    • 由于并发处理,流中元素的输出顺序并不固定,它们会按照处理速度被合并到最终的流中。因此,输出顺序无法保证与原始流的顺序一致。
  3. 常见使用场景

    • 适用于需要快速处理多个流,并且不关心数据输出顺序的场景。例如,从多个数据源并行获取数据,并将结果合并到一个流中进行处理。

示例:

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay

fun main() = runBlocking {
    val numbersFlow = flowOf(1, 2, 3)

    val resultFlow = numbersFlow.flatMapMerge { number ->
        flow {
            emit("$number: First")
            delay(100)  // 模拟异步处理
            emit("$number: Second")
        }
    }

    resultFlow.collect { value ->
        println(value)
    }
}
// 输出
1: First
2: First
3: First
1: Second
2: Second
3: Second

解释:

在这个示例中,numbersFlow 发出 1, 2, 3 三个整数。flatMapMerge 操作符将每个数字映射为一个新的流,该流依次发出两个字符串("First""Second")。由于 flatMapMerge 并发处理的特性,虽然每个流内部的顺序保持不变,但多个流的输出顺序可以交错。

适用场景:

  • 高效并发处理:当需要并行处理多个流,并希望尽快收集和处理结果时,flatMapMerge 是理想的选择。它能够最大化地利用并发性能,特别是在处理时间不均匀的任务时。
  • 不关心顺序:在某些情况下,处理结果的顺序并不重要,或者顺序可以通过其他方式处理,这时使用 flatMapMerge 可以显著提高性能。

总结

flatMapMerge 通过并发执行和合并多个流的数据,它可以大大提高处理速度,特别适用于不需要保持严格顺序的场景。使用 flatMapMerge 可以让流处理逻辑更具伸缩性和响应速度。

flatMapLatest

flatMapLatest 是一个操作符,用于将一个流中的元素转换为多个流,并只保留最新发出的流,丢弃之前的流。先转化成Flow的Flow再顺序展开的操作符,flatMapConcat 也是顺序展开,它和 flatMapConcat 的区别是在于它的名字里边的Latest。mapLatest, transforLatest, 这两个也是顺序展开。但flatMapLatest并不是在手机和转发Flow的时候卡住上游Flow的生产。而是上游继续生产。 并且一旦下一个Flow生产出来,就终止当前Flow的收集。开始收集和转发这个刚生产的Flow,展开只是对多个Flow的合并的一种形式。它是把多个Flow的数据放在一个统一的Flow里发送。

flatMapLatest 的主要特点:

  1. 只保留最新的流

    • 当新的元素到达时,flatMapLatest 会取消之前生成的流,并启动一个新的流。因此,只有最新的流会继续发出数据,之前的流会被丢弃。
    • 这意味着如果在旧的流还没发出所有数据之前就有新的元素到达,旧的流会被取消,其余的数据不会被发出。
  2. 适用于处理快速变化的输入

    • flatMapLatest 非常适合处理用户输入、搜索建议、实时数据流等场景,在这些场景中,保持对最新数据的响应而不是处理过时的数据非常重要。
  3. 保持顺序性

    • 尽管会丢弃旧流,flatMapLatest 仍然按照输入流发出的顺序处理元素,但每个元素生成的新流是最新的。
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay

fun main() = runBlocking {
    val numbersFlow = flowOf(1, 2, 3)

    val resultFlow = numbersFlow.flatMapLatest { number ->
        flow {
            emit("$number: First")
            delay(100)  // 模拟异步处理
            emit("$number: Second")
        }
    }

    resultFlow.collect { value ->
        println(value)
    }
}
// 输出:
1: First
2: First
3: First
3: Second

解释:

numbersFlow 依次发出 1, 2, 3 三个整数。flatMapLatest 对每个数字生成一个新的流,该流会发出两个字符串("First""Second")。由于 flatMapLatest 只保留最新的流:

  • 当数字 1 发出时,流开始执行,并发出 1: First
  • 数字 2 很快发出,此时之前的流会被取消,新的流发出 2: First
  • 数字 3 发出时,流再次被取消,新的流发出 3: First3: Second

适用场景:

  • 用户输入处理:比如在搜索框中输入字符时,flatMapLatest 可以确保只处理用户最后输入的字符,取消之前的搜索请求。
  • 实时数据流:当只关心最新的数据更新,并希望立即响应时,flatMapLatest 是非常合适的。
  • 异步任务切换:当一个异步任务在启动前可能会被新的任务替换时,使用 flatMapLatest 可以确保只执行最新的任务。

总结

flatMapLatest 是处理快速变化的输入流和需要立即响应最新数据的场景的理想工具。通过只保留最新的流,flatMapLatest 可以有效避免不必要的计算和资源浪费,同时确保应用程序对最新输入保持高效的响应。

combie

combine 是一个操作符,用于将多个 Flow 合并在一起,生成一个新的 Flow,该新的 Flow 会在任意一个输入 Flow 发出新值时产生新的数据项。

combine 的主要特点:

  1. 合并多个流

    • combine 可以将两个或多个 Flow 合并成一个 Flow,并且合并后的 Flow 会在任意一个输入 Flow 发出新值时,使用最新的值进行计算,然后发出新的数据项。
  2. 保留每个流的最新值

    • 当某一个 Flow 发出新值时,combine 会将这个新值与其他 Flow 的最新值一起传递给提供的组合函数,从而生成新的值。
  3. 无序性

    • combine 不保证输入 Flow 之间的执行顺序,它只关注任意一个输入 Flow 的最新值,并合并所有流的最新值来产生新结果。
  4. 常见使用场景

    • combine 适合在需要同时监听多个数据源,并且每个数据源的更新都会影响最终结果的场景。例如,将用户输入与实时数据源结合生成动态搜索结果,或者将多个传感器数据结合在一起生成合成数据。

使用示例:

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay

fun main() = runBlocking {
    val flow1 = flowOf(1, 2, 3).onEach { delay(100) }
    val flow2 = flowOf("A", "B", "C").onEach { delay(150) }

    val combinedFlow = flow1.combine(flow2) { number, letter ->
        "$number$letter"
    }

    combinedFlow.collect { value ->
        println(value)
    }
}
// 输出:
1A
2A
2B
3B
3C

解释:

在这个示例中,flow1flow2 分别发出整数和字符串。combine 操作符会将它们的最新值结合在一起,并生成新的值。

  • 初始时,flow1 发出 1flow2 发出 A,结果生成 1A
  • 接着,flow1 发出 2 时,flow2 的最新值还是 A,所以生成 2A
  • 然后,flow2 发出 Bflow1 的最新值是 2,于是生成 2B
  • 如此继续,直到所有流都完成发射。

适用场景:

  • 实时数据合并:比如在金融应用中,可能需要将股票的价格流和交易量流结合起来生成新的指标流。
  • UI状态管理:当多个独立的状态源(如网络请求结果、用户输入等)需要组合在一起更新 UI 时
  • 多传感器数据融合:在物联网或机器人应用中,可能需要将多个传感器的读数合并在一起进行综合判断。

总结

combine 允许将多个流的最新值结合在一起,生成一个新的流。它在处理需要多个异步数据源联合产生结果的场景中非常实用。

combine 与 merge 的区别

mergecombine 都能把多个 Flow “汇聚”到一个下游,但它们的触发时机输出内容激活条件完全不同。

1. 发射(emit)时机

  • merge(flowA, flowB, …)

    • 任何一个上游流发射新元素,就会立即把这个元素“原封不动”地转发给下游。
    • 它不等待其他流,也不做任何配对或计算。
  • combine(flowA, flowB) { a, b -> … }

    • 首次发射要等待 所有上游流都至少发过一条数据后,才能做第一次合并。
    • 此后,任何一个上游流再发新值,就会拿到这条流的最新值 另一个流的最近一次值,一起调用合并 lambda,然后发射 lambda 的返回结果。

2. 输出值

  • merge

    • 输出类型与上游相同:merge(Flow<T>, Flow<T>) 结果仍然是 Flow<T>
    • 它只是把多路事件「摊平」成一条流。
  • combine

    • 输出类型由我们在 lambda 里决定:combine(Flow<A>, Flow<B>) { a, b -> C } 会得到 Flow<C>
    • 它把“最新的 A、最新的 B” 两份数据“拉链”成一个新值。

3. 示例

// merge:谁先来就发谁
val numbers = flowOf(1, 2, 3).onEach { delay(100) }
val letters = flowOf("A", "B", "C").onEach { delay(150) }

merge(numbers, letters)
  .collect { println(it) }
// 可能输出:1,A,2,B,3,C  (交错发射,但不合并)

// combine:等两边都有值后,任何一方再来就合并一次
val flowA = flowOf(10,20).onEach { delay(100) }
val flowB = flowOf("X","Y","Z").onEach { delay(150) }

combine(flowA, flowB) { a, b -> "$a/$b" }
  .collect { println(it) }
// 可能输出:10/X,20/X,20/Y,20/Z

4. 何时用

  • merge:只想把多条事件流「平铺混合」,不需要做任何状态同步或计算。
  • combine:我们需要「随时同步」多条流的最新状态,并基于二者做一次聚合(映射)后再发射新结果。

zip

用于将两个 Flow 合并在一起,生成一个新的 Flow,新生成的 Flow 中的每一个数据项是由两个输入 Flow 的对应项组合而成的。zip 操作符会等待两个 Flow 都发出数据后再组合它们,因此它具有“配对”性质。

zip 的主要特点:

  1. 配对合并

    • zip 操作符将两个 Flow 的对应元素组合在一起。每当两个流都发出了一个新的元素时,zip 会将这两个元素通过提供的组合函数合并成一个新值并发出。
  2. 等待配对

    • zip 会等待两个流都发出新值后才会继续进行下一次组合操作。如果其中一个流的元素发射速度比另一个慢,那么 zip 操作符会等待较慢的那个流发出新值。
  3. 只处理对齐的元素

    • 只有当两个流都发出元素时,这些元素才会被组合成一个新的值并发出。如果两个流长度不一致,zip 会在较短的流结束时停止发射数据。
  4. 适用场景

    • zip 适用于那些需要配对处理两个流的场景,例如将两组相关数据组合在一起,或处理需要等待多个数据源都准备好之后才能进行的操作。

示例:

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay

fun main() = runBlocking {
    val flow1 = flowOf(1, 2, 3).onEach { delay(100) }
    val flow2 = flowOf("A", "B", "C").onEach { delay(200) }

    val zippedFlow = flow1.zip(flow2) { number, letter ->
        "$number$letter"
    }

    zippedFlow.collect { value ->
        println(value)
    }
}
// 输出:
1A
2B
3C

解释:

在这个示例中,flow1flow2 分别发出整数和字符串。zip 操作符会将它们的对应元素组合在一起:

  • flow1 发出 1,然后 flow2 发出 A,组合后发出 1A
  • flow1 发出 2,接着 flow2 发出 B,组合后发出 2B
  • 依此类推,直到两个流中的元素都被处理完毕。

适用场景:

  • 数据同步处理:当需要同时处理两个同步的流时,zip 可以将它们的对应元素一一配对处理。例如,将来自两个不同传感器的数据配对处理。
  • 联合输入:在表单处理或多数据源输入的场景中,zip 可以将来自不同输入源的数据进行配对处理。
  • 对比分析:在某些分析场景中,可能需要将两个相关但独立的数据流结合起来进行对比,zip 操作符非常适合这种情况。

总结

zip 操作符是 Kotlin Flow API 中用于将两个流进行配对处理的工具,它在等待两个流的对应元素都准备好之后再合并它们,适合用于需要对齐并组合两个数据流的场景。通过 zip,可以轻松地将两个独立的数据流同步在一起,并按顺序对它们的元素进行联合处理。

combineTransform

用于将多个 Flow 合并成一个 Flow,并在每次任意一个 Flow 发出新值时,使用自定义的转换逻辑生成新的数据项。与 combine 不同的是,combineTransform 可以在合并数据的过程中执行复杂的操作或异步任务。就和map与transform的区别一样,combineTransform不是把结合完的数据提供出来,而是让使用方自己去发送数据,这样就可以不局限于只发送一次数据了。

combineTransform 的主要特点:

  1. 灵活的转换逻辑

    • combineTransform 允许在合并流的过程中,使用 emit 函数来发射任意数量的值。这使得可以在合并流的同时,进行一些复杂的转换操作,比如发射多个值或进行异步计算。
  2. combine 的区别

    • combine 是在每次任意一个 Flow 发出新值时,直接合并所有流的最新值并发出结果。而 combineTransform 则允许在获取到最新值后执行一些自定义逻辑,甚至可以发射多个值或执行其他异步操作。
  3. 适用场景

    • combineTransform 适用于那些需要在合并流的过程中进行复杂操作的场景。例如,在合并数据流的同时,还需要进行一些额外的计算或根据条件发射不同的值。

示例:

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay

fun main() = runBlocking {
    val flow1 = flowOf(1, 2, 3).onEach { delay(100) }
    val flow2 = flowOf("A", "B", "C").onEach { delay(150) }

    val combinedFlow = flow1.combineTransform(flow2) { number, letter ->
        emit("$number$letter")
        delay(50)  // 进行一些额外的操作
        emit("$number-$letter")
    }

    combinedFlow.collect { value ->
        println(value)
    }
}
// 输出:
1A
1-A
2A
2-A
2B
2-B
3B
3-B
3C
3-C

解释:

在这个示例中,flow1 发出整数,flow2 发出字符串。combineTransform 操作符将它们的最新值结合在一起,并使用 emit 发射了两个不同的值:

  • 每当 flow1flow2 的最新值被接收到,combineTransform 会先发射第一个组合的值,例如 1A,然后执行一些额外的操作(如延迟),再发射第二个值 1-A
  • 因此,输出中可以看到每个组合产生了两个值。

适用场景:

  • 复杂数据处理:当需要在合并流的过程中进行复杂的转换逻辑时,比如发射多个值或执行异步任务,combineTransform 提供了足够的灵活性。
  • 条件性发射:在某些情况下,可能希望根据一些条件动态决定要发射的值,combineTransform 允许在获取到所有最新值后进行任意的处理。

总结

适用于需要在合并多个流的同时进行复杂操作的场景。与 combine 相比,combineTransform 提供了更高的灵活性,可以在合并流的过程中执行额外的逻辑操作,是处理复杂数据流的理想选择。

学后检测

选择题

1. 关于 merge 操作符,以下说法正确的是?

A. 合并后的 Flow 按所有输入流的顺序顺序输出
B. 合并后的 Flow 只会输出最后一个输入流的数据
C. 合并后的 Flow 会交错、实时输出所有输入流发射的数据
D. 合并后的 Flow 必须等待所有流完成后才能输出数据

答案:C
解析: merge 是“混合并发”,多个 Flow 谁先 emit 谁先输出,交错无序。


2. flattenConcatflattenMerge 的主要区别是什么?

A. flattenConcat 并发处理所有子流,flattenMerge 串行
B. flattenConcat 保证顺序处理每个子流,flattenMerge 并发处理每个子流
C. 二者完全一样
D. flattenConcat 只能合并2个 Flow,flattenMerge 能合并任意个

答案:B
解析: flattenConcat是“一个接一个”串行;flattenMerge是并发执行子流。


3. 关于 flatMapLatest 操作符,下列说法正确的是?

A. 只处理第一个发射的子流
B. 所有子流并发执行,全部收集
C. 每当新元素到来,取消上一个流,仅收集最新流的数据
D. 顺序收集所有子流,必须等前一个全部完成

答案:C
解析: flatMapLatest 保证只保留“最新”流,丢弃旧的。


4. combinezip 的区别,以下哪项正确?

A. combine 必须等所有流发射完毕才输出
B. zip 会等待两个流的对应项,都发射后才组合输出
C. combine 输出顺序严格配对
D. zip 每当任一流有新值就输出

答案:B
解析: zip 是一一配对组合,等两个流都有新数据时才发射。


5. flatMapConcat 与 flatMapMerge 的区别正确的是?

A. flatMapConcat 并发子流,flatMapMerge 顺序
B. flatMapConcat 串行子流,flatMapMerge 并发子流
C. 二者行为完全一致
D. flatMapConcat 只能处理1个流

答案:B
解析: flatMapConcat 串行执行子流,flatMapMerge 并发执行子流。


6. combineTransform 和 combine 区别的本质是?

A. combineTransform 只能合并两个流
B. combineTransform 可以通过 emit 发射多个值甚至异步处理
C. combine 只能发射一个值,不能用 emit
D. B、C 同时正确

答案:D
解析: combineTransform 用 emit,可发射多个或异步;combine只能发射合并lambda的返回值。


判断题

7. merge 操作符能保证所有流的顺序不变(即flow1所有数据都发完,再发flow2)。

答案:错
解析: merge 是“交错并发”,顺序不保证。


8. flattenConcat 能用于保证所有子流顺序输出。

答案:对
解析: flattenConcat 是严格顺序,一个流一个流处理。


9. flatMapLatest 适合用在搜索框联想、只要最新请求结果场景。

答案:对
解析: flatMapLatest 可自动取消旧请求、只保留最新的响应。


10. zip 输出数量等于两个流中更短的那个流的元素数量。

答案:对
解析: zip 一一配对,对齐,短流结束就结束。


简答题

11. merge、combine、zip、flattenMerge 的主要应用场景分别是什么?举例说明。

答案参考:

  • merge:实时混合多个事件源,如多个网络流合并为一个输出通道。
    例:多个消息通道推送合并。
  • combine:每当任一流有新值,用所有流最新状态计算新结果。
    例:用户名输入和密码输入流合并生成表单校验状态流。
  • zip:两个流严格一一配对,同步场景。
    例:图片和文字流配对合成数据结构。
  • flattenMerge:子流并发处理,不关心顺序。
    例:多API并发请求、并合成一个下游流。

12. flattenConcat 和 flatMapConcat、flattenMerge 和 flatMapMerge 的区别?

答案参考:

  • flattenConcat/flattenMerge 作用在 “Flow<Flow”(流的流);flatMapConcat/flatMapMerge 作用在普通流,把每个元素map成流后合并(等价于 map + flattenConcat/flattenMerge)。
  • flatMapConcat = map + flattenConcat(串行);flatMapMerge = map + flattenMerge(并发)。

13. flatMapLatest 适合哪些场景?举1个实际案例。

答案参考:

  • 适合“只要最新数据/最新任务,丢弃未完成老任务”场景。
  • 案例:搜索框输入,每次输入都触发一次网络请求,flatMapLatest自动取消前一个,只保留最后一次输入的结果。

14. combine 和 combineTransform 的区别及使用场景?

答案参考:

  • combine:直接合并最新值,发射单一值。
  • combineTransform:可以在合并时异步/发射多个值/灵活处理。
  • combineTransform 适合有副作用/多次 emit/异步时。

15. zip 和 combine 有哪些区别?请对比说明。

答案参考:

  • zip:严格一一配对,对齐输出(流A第i个和流B第i个配对);短流结束就结束。
  • combine:任意一边新数据就和另一边“最新值”结合,输出频率更高,不会被较慢流卡住。

编程题

16. 用 Kotlin Flow 编写一个示例:模拟2个 API 并发返回结果,并用 flattenMerge/flatMapMerge 合并,要求输出顺序不保证一致。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val api1 = flow {
        emit("api1-start")
        delay(100)
        emit("api1-end")
    }
    val api2 = flow {
        emit("api2-start")
        delay(50)
        emit("api2-end")
    }

    // 方法1:flattenMerge
    val listFlow = listOf(api1, api2).asFlow().flattenMerge()
    println("flattenMerge output:")
    listFlow.collect { println(it) }

    // 方法2:flatMapMerge
    val mainFlow = flowOf(1, 2).flatMapMerge { id ->
        if (id == 1) api1 else api2
    }
    println("flatMapMerge output:")
    mainFlow.collect { println(it) }
}

输出:
顺序可能为 api1-start, api2-start, api2-end, api1-end 等,不保证顺序。


17. 编写一段 combineTransform 用法代码,实现每当两个输入流任一有新值时,发射两次输出(一次是合并值,一次是带“-extra”后缀的合并值)。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val flow1 = flowOf(1, 2).onEach { delay(100) }
    val flow2 = flowOf("A", "B").onEach { delay(150) }

    flow1.combineTransform(flow2) { num, letter ->
        emit("$num$letter")
        emit("$num$letter-extra")
    }.collect { println(it) }
}

输出:
1A
1A-extra
2A
2A-extra
2B
2B-extra


18. 编写一个 flatMapLatest 示例,模拟搜索场景,只保留最后一次搜索结果。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val keywordFlow = flowOf("a", "ab", "abc").onEach { delay(80) }

    keywordFlow.flatMapLatest { keyword ->
        flow {
            emit("Start search: $keyword")
            delay(100)
            emit("Result: $keyword")
        }
    }.collect { println(it) }
}

输出可能为:
Start search: a
Start search: ab
Start search: abc
Result: abc


19. 简述 zip 的输出长度由什么决定?并写一个代码说明。

答案参考:
zip 输出长度等于最短流长度。
示例:

val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf("A", "B")
flow1.zip(flow2) { n, l -> "$n$l" }.collect { println(it) }
// 输出:1A, 2B