你需要了解的 Flow 的扩展方法都在这里了

1,733 阅读6分钟

在 Kotlin 开发过程中,我们会遇到众多与flow相关的方法。在此,对这些方法进行汇总。以 Kotlin 1.6.1 的源码为例,flow的扩展方法被放置在不同的文件中,如下图所示。下面将依据不同的文件来介绍这些不同的扩展方法。

屏幕截图 2024-08-11 213614.png

FlowKt_BuildersKt

FlowKt_BuildersKt文件中,主要存放着与创建flow相关的方法,具体包括:flowasFlowflowOfemptyFlowchannelFlow以及callbackFlow。这些扩展方法的代码示例如下:

// 使用 flow 创建一个 Flow 流
flow {
    // 创建一个冷流 flow
    emit(1)
    // 内部不能使用 withContext
    withContext(Dispatcher.IO) {
        emit(2)
    }
}
// 使用 asFlow 将集合转换为 Flow 流。除了集合外,还在序列、数组、有返回值的方法上使用
listOf(1, 2, 3).asFlow()

// 创建一个发射指定值的 Flow
flowOf(1, 2, 3)

// 创建一个空的 Flow
emptyFlow()
// 创建一个 channelFlow
channelFlow {

}
// 创建一个 callbackFlow
callbackFlow {

}

flowasFlowflowOfflowOfemptyFlow 这些扩展方法比较好理解。这里比较难理解的是 flowchannelFlowcallbackFlow 方法。虽然它们都是创建一个 flow,但是所创建的 flow 实现和使用上都有很大的不同。channelFlowcallbackFlow的区别如下:

  • channelFlow,默认带 64个缓存区;使用 sendtrySend 发送数据
  • callbackFlow,可以在 flow 外部发送数据,同时支持 awaitClose

详细地可以看[译]轻松学习Kotlin的Flow、ChannelFlow和CallbackFlow

FlowKt_FlowChannelsKt

FlowKt_FlowChannelsKt 文件中主要存放了 channel 与 flow 相互转换的方法,具体包括 receiveAsFlowconsumeAsFlowproduceIn。除此之外还有一个 emitAll。代码示例如下:

val sourceFlow = flow {
   emit(1)
   emit(2)
   emit(3)
}
// emitAll 发射一个流
val targetFlow = flow {
   emitAll(sourceFlow)
   emit(4)
}
// receiveAsFlow 将 Channel 转换成一个 flow
val channel = Channel<Int>()
channel.receiveAsFlow()

// consumeAsFlow 也是将 Channel 转换成一个 flow
channel.consumeAsFlow()

// produceIn 将 flow 转化为 ReceiverChannel;
// ReceiverChannel 是用来接收数据的 channel
flow {
  emit(0)
}.produceIn(coroutineScope)

从上面的代码示例中可以看到,receiveAsFlowconsumeAsFlow 都会把 Channel 转化为 Flow。但是receiveAsFlow 转化后的 Flow 能多次 collect;而 consumeAsFlow 转化后的 Flow 只能一次 collect,多次会 crash。代码示例如下:

val receiveAsFlow = channel.receiveAsFlow()
receiveAsFlow.collect {
    // 当后面 collect 后,这里就不执行了
    println("receiveAsFlow collect1 $it") 
    delay(20)
    receiveAsFlow.collect { // 可以多次 collect
        println("receiveAsFlow collect2 $it")
    }
}

val consumeAsFlow = channel.consumeAsFlow()
consumeAsFlow.collect {
    println("consumeAsFlow collect1 $it")
    delay(20)
    consumeAsFlow.collect { // 这里会 crash
        println("consumeAsFlow collect2 $it")
    }
}

FlowKt_FlowCollectionKt

FlowKt_FlowCollectionKt 文件中只有三个方法,分别是 toListtoSettoCollection。它们的作用是把 Flow 转化为对应的集合类型。代码示例如下:

 val numbersFlow = flowOf(1, 2, 3)
val listResult = numbersFlow.toList() // 转化为 List
val setResult = numbersFlow.toSet() // 转化为 Set
val customCollection = numbersFlow.toCollection(ArrayList()) // 将结果添加到集合

FlowKt_FlowCollectKt

FlowKt_FlowCollectKt 主要存放和收集流中元素的相关方法,具体包括 collectlaunchIncollectIndexedcollectLatest 方法。代码示例如下:

// collect 触发流动
flowOf(1, 2, 3).collect() 

// launchIn 在指定的协程作用域中启动一个协程来收集流中的元素
flow {
   emit(1)
   emit(2)
 }.launchIn (lifecycleScope)
// 等同于下面的代码
scope.launch {
    flow {
       emit(1)
       emit(2)
    }.collect()
}

// 触发流动,除此之外 collectIndexed 会带有额外的 index 信息
val numbersFlow = flow {
   emit(1)
   emit(2)
   emit(3)
}
numbersFlow.collectIndexed { index, number ->  
    println("Index: $index, Number: $number")  
}

// collectLatest 只收集流中的最新元素,同时取消对旧元素的处理
// 如果 `emit(1)` 后 `emit(2)`;如果 1 的值没有处理完就有新的value 2,那么之前的操作会被取消。
flow {
   emit(1)
   emit(2)
}.collectLatest { // 这里只会打印 2
   delay(500)
   println(it)
}

FlowKt_ContextKt

FlowKt_ContextKt 文件主要存放处理中间操作的方法,具体包含 bufferconflateflowOn cancellable。代码示例如下:

val numbersFlow = flow {
   for (i in 1..10) {
       delay(100)
       emit(i)
   }
}
// buffer 会给 Flow 加上缓存,缓存大小默认是 64。
// 1. 当缓存没有满时,同时 collect 代码块还没执行完,会把 emit 的值保存在缓存中
// 2. 当缓存没有满时,同时 collect 代码块执行完了,会通知  collect 代码块处理下一个值
// 3. 当缓存满,同时 collect 代码块还没执行完,这时会挂起 emit 方法
// 4. 当缓存满, collect 代码块执行完了,会通知  collect 代码块处理缓存里的值
numbersFlow.buffer().collect { number ->
   println(number)
   delay(500)
}

// conflate 不会阻塞发送,也不会收集旧的值。它等同于 `buffer(CONFLATED)`
numbersFlow.conflate().collect { number ->
   println(number)
   delay(500)
}

// flowOn 指定上游操作的执行线程
withContext(Dispatchers.Main) {
    val singleValue = intFlow 
        .map { ... } // 在 IO 线程执行
        .flowOn(Dispatchers.IO)
        .filter { ... } // 在 Default 线程执行
        .flowOn(Dispatchers.Default)
        .single() // 在主线程执行
}

// cancellable 让流可以取消,flow{} 和 shareflow 默认都是可以被取消的
numbersFlow.cancellable()

FlowKt_CountKt

FlowKt_CountKt 文件只有一个方法 count,用来返回流发送值的个数。代码示例如下:

// 发送值的个数,这里 count 为 2
val count = flow {
    emit(1)
    emit(2)
}.count()
// 满足条件的发送值的个数,这里 count 为 1
val count = flow {  
    emit(1)  
    emit(2)  
}.count {  
    it > 1  
}

FlowKt_DelayKt

FlowKt_DelayKt 文件有两个方法 debouncesample,它们都是用于 Flow 中时间的控制。debounce 可以获取给定时间内最新的值,可以用来防止抖动;sample,获取周期内发出的最新值,用来采样。代码示例如下:

// 使用 debounce 打印 3 4 5
flow {
    emit(1)
    delay(90)
    emit(2)
    delay(90)
    emit(3)
    delay(1010)
    emit(4)
    delay(1010)
    emit(5)
}.debounce(1000)
    .collect {
        println(it)
    }
// 使用 sample 打印 3 4    
flow {
    emit(1)
    delay(90)
    emit(2)
    delay(90)
    emit(3)
    delay(1010)
    emit(4)
    delay(1010)
    emit(5)
}.sample(1000)
    .collect {
        println(it)
    }    

FlowKt_DistinctKt

FlowKt_DistinctKt 文件有两个方法 distinctUntilChangeddistinctUntilChangedBy ,它们的作用都是用来去重的。代码示例如下:

// distinctUntilChanged 如果连续两个值一样,则会跳过发送。
// 通过这种方式来确保流中连续发出的相同元素只被发射一次
val numbersFlow = flow {
    emit(1)
    emit(1)
    emit(2)
    emit(2)
    emit(3)
}
numbersFlow.distinctUntilChanged().collect { number ->
    println(number)
}

// distinctUntilChangedBy 可以指定用来判断的 key
val peopleFlow = flow {
    emit(Person("Alice", 25))
    emit(Person("Bob", 30))
    emit(Person("Alice", 25))
    emit(Person("Charlie", 35))
}
peopleFlow.distinctUntilChangedBy { person -> person.name }.collect { person ->
    println(person.name)
}

FlowKt_EmittersKt

FlowKt_EmittersKt 文件主要存放 flow 的一些回调,比如 onStartonCompletiononEmpty,以及 transform 方法。代码示例如下:

flow {
    emit(1)
    emit(2)
    1 / 0 // 抛出异常,onCompletion 还是会被调用
    emit(3)
}.onStart {
    // collect 执行前的回调
    println("Flow start")
}.onCompletion {
    // 在 flow 取消或者结束时调用,类似于 finally {}
    println("Flow completed")
}.onEmpty {
    // 当流没有 emit 任何值时调用
    println("Flow Empty")
}.collect {
    println("collect $it")
}
// 打印结果如下:
Flow start
collect 1
collect 2
Flow completed
异常

除了上面 flow 的回调相关方法外,还有一个 transform 方法,用来做 flow 数据的转换。除此之外,还有一个 map 方法,也是做转换的。这里就有一个问题,它们的区别是什么?

简单来说,就是在 transform 中使用 emit 来发送元素,因此可以增加、修改、删除(不发送)元素;而在 map 中只能映射元素为新的元素。代码示例如下:

 flow {
    emit(1)
    emit(2)
    emit(3)
}.map {
    it * 2
}.transform { value ->
    if (value > 1)
      emit(value)
    else 
      emit(-1)  
}.collect()

可以看到 transform 可以对输入的元素做任意的转换,而 map 只能将一个元素转换为新的元素。实际上在源码中,map 就是通过 transform 来实现的。

FlowKt_TransformKt

FlowKt_TransformKt 文件主要存放转换相关的方法,具体包含 filterfilterNotfilterIsInstancefilterNotNullmapmapNotNullonEach 方法,它们内部都是通过 transform 来实现的。除此之外,这个文件还包含 withIndexscanrunningFold runningReduce 方法,也是对数据做转化,但它们不是用 transform 实现的 。代码示例如下:

 val numbersFlow = flow {
   emit(1)
   emit(2)
   emit(3)
   emit(4)
   emit(5)
}
// filter 筛选出符合条件的值,这里获取偶数
val filteredFlow = numbersFlow.filter { number ->
   number % 2 == 0
}
// filterNot 筛选出不符合条件的值,这里获取奇数
val filteredFlow = numbersFlow.filterNot { number ->
   number % 2 == 0
}
// filterIsInstance 筛选出指定类型的值
val mixedFlow = flow {
   emit(1)
   emit("two")
   emit(3)
   emit("four")
}
val intFlow = mixedFlow.filterIsInstance<Int>()
// filterNotNull 筛选出不为null的值
val nullableFlow = flow {
   emit(null)
   emit(1)
   emit(null)
   emit(2)
}
val nonNullFlow = nullableFlow.filterNotNull()
// map 一对一转换; mapNotNull 会过滤掉转换后为 null 的值
flow {
    emit(1)
    emit(2)
    emit(3)
}.map {
    it * 2
}
// 在每个元素被 emit 时调用
numbersFlow.onEach { number ->
   println("Processing $number")
}.collect { value ->
   println(value)
}


val numbersFlow = flow {
    emit(1)
    emit(2)
    emit(3)
}
// withIndex 获取带有 index 的流
numbersFlow.withIndex().collect { (index, number) ->
    println("Index: $index, Number: $number")
}

// scan 从初始值开始 执行遍历,并将结果作为下个执行的 参数
// 打印的结果为 [0, 1, 3, 6]
val result = numbersFlow.scan(0) { accumulator, number ->
    accumulator + number
}.toList()
println(result)

// runningFold 和 scan 的功能的一样的,scan 就是 runningFold 的别名
val result = numbersFlow.runningFold(0) { accumulator, number ->
    accumulator + number
}.toList()

// runningReduce 和 scan 的作用是一样的,区别是 runningReduce 不需要设置初始值
val result = numbersFlow.runningReduce { accumulator, number ->
    accumulator + number
}.toList()
println(result)

FlowKt_ErrorsKt

FlowKt_ErrorsKt 文件里的方法主要是处理异常的,具体有 catchretryretryWhen 方法。代码示例如下:

flow {
    emit(1)
    throw RuntimeException("Error in flow")
}.retry(3) { // 重试3次,并控制是否执行
      if (it is IOException){
          true
      }else {
          false
      }
}.catch { e -> // 重试3次都失败后,会调用到这里。需要注意 catch 必须在 retry 后面
    println("Caught exception: ${e.message}")
    emit(0) // 返回默认值
}.collect{

}
// retryWhen 满足一定条件就重试,其中 cause 为异常原因,attempt 为重试次数,从0开始
flow<Int> {
    emit(0)
    throw IOException("")
}.retryWhen { cause, attempt -> 
    ...
}

FlowKt_limitKt

FlowKt_limitKt 文件中存放对 flow 元素数量进行处理的方法,具体包含 dropdropWhiletaketakeWhiletransformWhile 方法。代码示例如下:

 val numbersFlow = flow {
   emit(1)
   emit(2)
   emit(3)
   emit(4)
   emit(5)
}
// drop 跳过指定数量的值,如果参数为负数时,则报错
val droppedFlow = numbersFlow.drop(2)

// 这个操作符有点特别,和`filter` 不同,它是找到第一个不满足条件的值,返回其和其之后的值。  
// 如果首项就不满足条件,则是全部返回。下面的例如会获取到 3 4 5
val droppedFlow = numbersFlow.dropWhile { number ->
   number < 3
}

// take 获取指定数量的值,如果参数设置为负数,则报错
numbersFlow.take(3)
// 找第一个不满足条件的项,但是取其之前的值。和 dropWhile 相反。
// 如果第一项就不满足,则为返回空流。下面的例如会获取到 1 2
numbersFlow.takeWhile { number ->
   number < 3
}
// transformWhile 和 transform 类似。
// 区别是返回值为 false 时不再进行后续变换; 为 true 时则继续执行
numbersFlow.transformWhile { number ->
   val result = number * 2
   emit(result)
   number < 4
}

FlowKt_mergeKt

FlowKt_mergeKt 文件主要包含合并相关的方法,具体有 flattenMergeflatMapMergeflattenConcatflatMapConcattransformLatestflatMapLatestmapLatest 方法。代码示例如下:

// flattenConcat 以顺序方式将给定的流扁平化为单个流,而不交织嵌套流。
// 下面的示例打印结果为 1 2 3
val flow1 = flow { emit(1); delay(100); emit(2) }
val flow2 = flow { emit(3); delay(200) }
flow {
    emit(flow1)
    emit(flow2)
}.flattenConcat()
.collect { value ->
    println(value)
}
// flattenConcat 的效果等同于下面的操作
flow {
  emit(1)
  delay(100)
  emit(2)
  emit(3)
  delay(200)
}

// flattenMerge  设置参数 concurrency 为 1 时,内部使用 flattenConcat 实现,做顺序收集 ;
// concurrency 设置大于 1 时,做并发收集.
// 下面的示例打印结果为 1 3 2
flow {
    emit(flow1)
    emit(flow2)
}.flattenMerge(2)
.collect { value ->
    println(value)
}

// flatMapConcat 等同于 map +  flattenConcat,先执行 map,再执行 flattenConcat
flow {
    emit(flow1)
    emit(flow2)
}.flatMapConcat {
   it.map {
       it * 2
   }
}.collect { value ->
    println(value)
}

// flatMapMerge 等同于 map + flattenMerge
flow {
    emit(flow1)
    emit(flow2)
}.flattenMerge(2) {
   it.map {
       it * 2
   }
}.collect { value ->
    println(value)
}

// merge, 把多个 flow 并发合并成一个 flow
// 打印结果为 1 a b c 2
val numberFlow = flow {
    emit(1)
    delay(100)
    emit(2)
}
val stringFlow = flow {
    emit("a")
    emit("b")
    emit("c")
}
listOf(numberFlow, stringFlow).merge()
    .collect { value ->
        println(value)
    }

// transformLatest 用来转换最新的值,如果 `emit(1)` 后执行 `emit(2)`;
// 如果 1 的值没有处理完就有新的value 2,那么之前的操作会被取消。
// 比如下面的示例只会打印 2
flow {
    emit(1)
    emit(2)
}.transformLatest {
    delay(100)
    emit(it)
}.collect {
    println(it)
}

// mapLatest,映射最新的值,如果 `emit(1)` 后 `emit(2)`;
// 如果 1 的值没有处理完就有新的value 2,那么之前的操作会被取消。
// 比如下面的示例只会打印 2
flow {
    emit(1)
    emit(2)
}.map {
    delay(100)
    it
}.collect {
    println(it)
}

// flatMapLatest 映射最新的flow,之前的值flow会被取消
// 比如下面的示例只会打印 2
flow {
    emit(1)
    emit(2)
}.flatMapLatest {
    flow {
        delay(100)
        emit(it)
    }
}.collect {
    println(it)
}

FlowKt_MigrationKt

FlowKt_MigrationKt 内部存放的是被废弃的api,我们不需要关注。

FlowKt_reduceKt

FlowKt_reduceKt 文件主要包含获取 flow 中结果的方法,具体包含 reducefoldsinglesingleOrNullfirstfirstOrNulllastlastOrNull。代码示例如下:

// reduce 从第一个元素开始累加值,并对当前累加器值和每个元素应用操作。
// 如果流为空,则抛出NoSuchElementException。reduce 与 scan 类似,区别是
// scan 返回的是流,而 reduce 直接返回的是结果
val numbersFlow = flow {
    emit(1)
    emit(2)
    emit(3)
}
val reducedValue = numbersFlow.reduce { accumulator, value ->
    accumulator + value
}
println(reducedValue)

// fold 和 reduce 类似,区别是需要提供一个初始值
numbersFlow.fold(0) { accumulator, value ->
    accumulator + value
}

// single 要求流只能发射一个元素
// 对于空流抛出NoSuchElementException,对于包含多个元素的流抛出IllegalStateException。
val result = flow {
    emit(1)
}.single()

// singleOrNull 接收流发送的第一个值 ,可以为空,发出多值的话除第一个,后面均被置为null
val result = flow {
    emit(1)
    emit(2)
    emit(3)
}.singleOrNull()

// first 返回流发出的第一个元素;如果流为空,则抛出NoSuchElementException。
val result = flow {
    emit(1)
    emit(2)
    emit(3)
}.first()

// firstOrNull 返回流发出的第一个元素,如果流为空,则返回null。
val result = flow {
    emit(1)
    emit(2)
    emit(3)
}.firstOrNull()

// last 返回流发出的最后一个元素。如果流为空,则抛出NoSuchElementException。
val result = flow {
    emit(1)
    emit(2)
    emit(3)
}.last()

// lastOrNull 返回流发出的最后一个元素,如果流为空,则返回null
val result = flow {
    emit(1)
    emit(2)
    emit(3)
}.lastOrNull()

FlowKt_ShareKt

FlowKt_ShareKt 存放与共享流相关的方法,具体有 shareInstateInasSharedFlowasStateFlowonSubscription。代码示例如下:

// shareIn、stateIn 都会将一个冷流(Cold Flow)转换为热流
// 其中 stateIn 是转化为状态流;shareIn 是转化为共享流
val numbersFlow = flow {  
    emit(1)  
    emit(2)  
    emit(3)  
}.stateIn(scope)

// asSharedFlow、asStateFlow 将现有的 SharedFlow 或者 StateFlow 
// 转换为只读形式,增强封装性。
val sharedFlow = MutableStateFlow<Int>(0)  
sharedFlow.asSharedFlow()  
sharedFlow.asStateFlow()

// onSubscription  SharedFlow、StateFlow 建立订阅之后的回调
sharedFlow.onSubscription {  
  
}

FlowKt_zipKt

FlowKt_zipKt 文件存放 flow 元素组合的方法,具体有:combinecombineTransformzip。代码示例如下:

// combine 将多个流的最新值结合起来,每当任何流发射新值时都会触发。
// 下面的代码示例打印结果为 
// 1 - A
// 2 - A
// 2 - B
val flow1 = flow {
    emit(1)
    delay(100)
    emit(2)
}
val flow2 = flow {
    emit("A")
    delay(200)
    emit("B")
}
val combinedFlow = flow1.combine(flow2) { num, str ->
    "$num - $str"
}
combinedFlow.collect { combinedValue ->
    println(combinedValue)
}

// combineTransform 与 `combine` 类似,但提供了更灵活的方式来处理合并的流。
// 可以在合并过程中发射多个值。下面的代码示例打印结果为 
// 1 - A
// true
// 2 - A
// true
// 2 - B
// true
val combinedFlow = flow1.combineTransform(flow2) { num, str ->
    emit("$num - $str")
    emit(true)
}
combinedFlow.collect { combinedValue ->
    println(combinedValue)
}

// zip 将多个流的值一一对应地合并。只有在所有输入流都发射了新值时才会触发合并.
// 下面的代码示例打印结果为 
// 1 - A
// 2 - B
val combinedFlow = flow1.zip(flow2) { num, str ->
    "$num - $str"
}
combinedFlow.collect { combinedValue ->
    println(combinedValue)
}