第三节:Flow的进阶操作符

27 阅读7分钟

前言:

前两篇文章我们介绍了Flow的基础知识以及Flow的基础操作符。什么是冷流?什么是热流?onEach、filter、map、debouce、catch等基础操作符,感兴趣的同学可以去阅读下这两篇文章:
第一节:Flow的基础知识
第二节:Flow的基础操作符
这篇文章我们来介绍下Flow中一些比较高级的操作符,熟练的掌握这些操作符,可以让我们在实际开发中,像使用网络请求或者异步数据处理带来很大的方便。

1.操作符flowOn

flowOn操作符用于更改流发射的上下文(即改变流执行的协程调度器)。它会改变流上游的协程调度器,但不会影响下游。如下代码示例,我们使用flow操作符,让流的数据发送在子线程中执行,流的收集在主线程中执行(main函数在主线程中运行):

fun main() = runBlocking {
    flow {
        // 网络请求或其它耗时操作
        emit("flowOn")
        println("emit threadName = ${Thread.currentThread().name}")
    }
        .flowOn(Dispatchers.IO)
        .collect {
            println(println("collect threadName = ${Thread.currentThread().name}"))
        }
}

// 输出
emit threadName = DefaultDispatcher-worker-1
collect threadName = main
kotlin.Unit

2.操作符lauchIn

launchIn 是 Kotlin Flow 的终端操作符 ,用于在指定协程作用域中异步启动流收集 。它与 collect 功能相同,但通过结构化并发提供更简洁的生命周期管理。

// 传统方式(可能阻塞)
scope.launch { 
    flow.collect { ... }
}

// 使用 launchIn(立即返回,不阻塞)
flow.onEach { ... }.launchIn(scope) 

流收集与作用域绑定,自动取消:

val scope = CoroutineScope(Dispatchers.Main)

flow
    .onEach { updateUI(it) }
    .launchIn(scope) // 当 scope 取消时,流自动取消

// 取消整个作用域(自动终止流收集)
scope.cancel() 

launchIn(lifecycleScope) // 绑定 Activity/Fragment 生命周期
launchIn(viewModelScope) // 绑定 ViewModel 生命周期

常用的调用示例:

dataFlow
    .onStart { showLoading() }
    .onEach { updateData(it) }
    .catch { handleError(it) }
    .onCompletion { hideLoading() }
    .launchIn(viewModelScope)

3.操作符flatMapConcat

前面我们所介绍的内容都是在一个flow上进行操作的,从这里我们将开始介绍对多个流组合使用的操作符。flatMapConcat操作符用于将前一个flow与后一个flow映射,拼接成一个flow,且保证两个flow中的数据执行始终是并行的。

fun main() = runBlocking {
    flowOf(200L, 100L)
        .flatMapConcat { timeMills ->
            flow {
                delay(timeMills)
                emit("first: $timeMills ms")
                emit("second: $timeMills ms")
            }
        }
        .collect { println(it) }
}
// 输出
first: 200 ms
second: 200 ms
first: 100 ms
second: 100 ms

flatMapConcat操作符,接收其上游的数据,并返回一个新的flow:

public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R> =
    map(transform).flattenConcat()

4.操作符flatMapMerge

flatMapMerge和flatMapConcat在用法上几乎是一致的,从命名上我们可以看出它们的区别在于merge是合并的意思,而concat是连接的意思。合并是不保证执行顺序的,而连接是可以保证前后数据的顺序执行。同样的我们使用上面flatMapConcat的示例将其操作符改为flatMapMerge,输出结果就会完全不同:

fun main() = runBlocking {
    flowOf(200L, 100L)
        .flatMapMerge { timeMills ->
            flow {
                delay(timeMills)
                emit("first: $timeMills ms")
                emit("second: $timeMills ms")
            }
        }
        .collect { println(it) }
}

// 输出
first: 100 ms
second: 100 ms
first: 200 ms
second: 200 ms

从这里我们可以看出flatMapMerge不会在保证两个flow前后数据发送的顺序,而是优先执行当前需要处理的任务。

5.操作符flatMapLatest

flatMapLatest也是一个组合流,用法上和前两个flatMap操作符都是一致的。当前一个flow中的数据传递到后一个flow中会立刻进行处理,但如果前一个flow中的下一个数据要发送了,而后一个flow中上一个数据还没处理完,则会直接将剩余逻辑取消掉,开始处理最新的数据。

fun main() = runBlocking {
    flow {
        emit("first")
        delay(100)
        emit("second")
        delay(50)
        emit("third")
    }
        .flatMapLatest { value ->
            flow {
                delay(150)
                emit("value: $value")
            }
        }
        .collect { 
                println("collect start.")
                println(it)
                println("collect finish.")
        }
}

// 输出
collect start.
value: third
collect finish.

从示例代码我们可以看到,flow1发送了3条数据,但前两条数据发送结束后flow2中的数据还没有处理完成,前两条数据都被丢弃了,只有第三条数据被接收处理了。

6.操作符zip

zip操作符同样可以将两个flow拼接成一个flow,它也是并发执行两个任务,最终的执行时长以耗时较长的任务为准。如下代码示例:

fun main(): kotlin.Unit = runBlocking {
    measureTime {
        flow {
            delay(1000)
            emit("first")
        }.zip(flow {
            delay(3000)
            emit("second")
        }) { first, second ->
            println("$first $second")
        }.collect()
    }.apply { println("spend $this ms") }
}

// 输出
first second
spend 3.084227200s ms

zip操作符还有个特点就是,当两个flow中的发射的数据量不一致时,如果其中一个flow中的数据已完全处理完成,就会立即终止运行。如下代码示例:

fun main() = runBlocking {
   flowOf(1, 2, 3)
       .zip(flowOf("a", "b")) { pre, next ->
           println("pre = $pre, next = $next")
       }.collect()
}

// 输出
pre = 1, next = a
pre = 2, next = b

flow1中有3条需要处理的数据,flow2中只有两条需要处理的数据,当flow2中数据处理完成后,当前数据流也已经终止了。

7.操作符combine

介绍完zip操作符,再来介绍combine操作符就比较简单了。首先它和zip操作符一样都是并行执行两个flow,这里就不重复上述的示例代码了,感兴趣的同学可以将上述zip操作符中的第一个代码片段改为combine自行尝试下。这里我们主要来讲述将上述第二个代码片段中的zip操作符替换为combine操作符,如下示例代码:

fun main() = runBlocking {
   flowOf(1, 2, 3)
       .combine(flowOf("a", "b")) { pre, next ->
           println("pre = $pre, next = $next")
       }.collect()
}

// 输出
pre = 1, next = a
pre = 2, next = b
pre = 3, next = b

从输出结果我们很容易看出zip操作符和combine操作符的区别。即当两个flow中执行的数据任务量不同时,当其中一个flow中的数据处理完成后,当前数据流并不会立即停止,而是将执行完成数据flow中的最后一个数据,同当前还在处理数据的flow数据一同下发。到这里关于组合流的基本使用我们就介绍完,最后我们再介绍另外两个比较重要的操作符buffer及conflate。

8.操作符buffer

默认情况下,flow数据流的发送和接收在同一个协程作用域中,我们可以理解为:数据的发送和接收是串行的,即当一条数据发送和接收处理完成后,我们才会接着处理下一条数据,如下代码示例:

fun main() = runBlocking {
    flow {
        for (i in 0 until 3) {
            emit(i)
            delay(500)
        }
    }.onEach { println("value:$it is ready.") }
        .collect {
            delay(500)
            println("collect $it")
        }
}

// 输出
value:0 is ready.
collect 0
value:1 is ready.
collect 1
value:2 is ready.
collect 2

这种串行的效果,有时候可能并不能够满足我们实际的开发需求。而buffer的存在为我们很好的解决了这个问题。buffer操作符可以让我们的数据发送和接收互不影响,它为数据提供了一个缓存区,数据的发送和收集运行在不同的协程作用域中。这样flow函数只需要关注数据的发送,并不需要关注这条数据是否处理完成,collect函数只管负责从buffer中收集数据就可以了。接着上面的示例,我们使用buffer操作符再看下效果:

fun main() = runBlocking {
    flow {
        for (i in 0 until 3) {
            emit(i)
            delay(500)
        }
    }.onEach { println("value:$it is ready.") }
        .buffer()
        .collect {
            delay(500)
            println("collect $it")
        }
}

// 输出
value:0 is ready.
value:1 is ready.
collect 0
value:2 is ready.
collect 1
collect 2

由输出结果我们可以看到,数据的发送和接收不再是串行执行。

9.操作符conflate

conflate操作符是基于buffer操作符实现的,单单从源码上看,仅仅是将buffer函数中的capacity参数的值固定为CONFLATED。

public fun <T> Flow<T>.conflate(): Flow<T> = buffer(CONFLATED)

在上面我们对比了zip和combine操作符的区别,这里为了说清楚conflate操作符。我们使用collectLatest操作符做对比,如下代码示例:

fun main() = runBlocking {
    flow {
        var count = 0
        while (true) {
            emit(count)
            delay(1000)
            count++
        }
    }.collectLatest {
            println("start  $it collect.")
            delay(2000)
            println("collect $it finish.")
        }
}

// 输出
start  0 collect.
start  1 collect.
start  2 collect.

从输出结果我们可以看到,当上游有新的数据发送,而下游的任务还没有处理完成,下游的任务就被取消了,collect finish不会被执行到。而conflat操作符就是用来解决这个问题的。conflat操作符会保证我们总会将流的发送和收集执行完毕,才会执行下一条任务,如果有新的任务来了,缓存中任有未处理的数据,那么就将旧的正在处理的任务丢弃转而执行新的任务。

fun main() = runBlocking {
    flow {
        var count = 0
        while (true) {
            emit(count)
            delay(1000)
            count++
        }
    }.conflate()
        .collect {
            println("start  $it collect.")
            delay(2000)
            println("collect $it finish.")
        }
}

// 输出
start  0 collect.
collect 0 finish.
start  1 collect.
collect 1 finish.
start  3 collect.
collect 3 finish.
start  5 collect.
collect 5 finish.

由这里的输出结果我们可以看到2,4被丢弃了,但下游的任务是在完全处理完成以后再去处理其它的任务。

总结:

到这里flow中的一些比较常用的高级操作符就介绍完了。对于

  • flatMapConcat : flatMapMerge
  • zip : combine
  • buffer : conflat : collectLatest
    我们能够清楚的掌握它们之间的区别。这对我们在实际开发中使用flow会很有帮助。
    文章的质量可能并不高,但我始终相信,好记性,不如烂笔头。对于我自己而言也是做个开发笔记,感谢您的阅读!
    参考文章:guolin.blog.csdn.net/article/det…