flow 操作符全解析

460 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

简易使用

声明 flow:

fun dataSet() = flow {
    for (i in 1..3) {
        emit(i)
    }
}

监听获取数据:

    GlobalScope.launch {
        dataSet().collect { value ->
            println(value)
        }
    }

日志输出:

I/System.out: value1
I/System.out: value2
I/System.out: value3

好了,正文开始。

flowOn

更改流发射的上下文。

fun dataSet() = flow {
    println("threadName: ${Thread.currentThread().name}")
    emit(1)
}.flowOn(Dispatchers.IO)

日志输出:

I/System.out: threadName: DefaultDispatcher-worker-2

若将 Dispatchers.IO 改为 Dispatchers.Main

则输出的结果为:

I/System.out: threadName: main

launchIn

更改收集流的协程作用域。

    GlobalScope.launch {
        dataSet().onEach { value ->
            println("threadName: ${Thread.currentThread().name}")
            println("value: $value")
        }.launchIn(CoroutineScope(Dispatchers.Main))
    }

日志输出:

I/System.out: threadName: main
I/System.out: value: 1

不过这里有一点需要注意,就是 launchIncollect 是不能共存的,若使用了 launchIn,那么获取数据则需要使用 onEach

onEachlaunchIn 并不是要固定使用的,onEach 也可以和 collect 配套使用,一般在 onEach 中进行一些间隔处理,例如:

    GlobalScope.launch {
        dataSet().onEach { 
            delay(100)
        }.collect {
            println("value: $it")
        }
    }

cancellable

让 flow 标记为是可以取消的。

默认的情况下,flow 的执行是不可以取消的,即使自动调用了 cancel(),🌰:

        (1..3).asFlow().collect { value ->
            if (value == 2) cancel()
            println("value: $value")
        }

日志输出:

I/System.out: value: 1
I/System.out: value: 2
I/System.out: value: 3

加上 cancellable 就可以取消了:

        (1..3).asFlow().cancellable().collect { value ->
            if (value == 2) cancel()
            println("value: $value")
        }
I/System.out: value: 1
I/System.out: value: 2

buffer

缓存所提交的数据。

flow 的执行流程,就生产者和消费者一样,上游产出,下游消费,但是,在默认的情况下,都是生产一个,消费一个,再生产一个,再消费一个,这样进行的。若是生产和消费的速度都很快的话,那倒没有什么问题,但是,若生产速度大于消费速度,就会导致生产效率被强制降低了,因为生产者会一直等到消费者消费完再生产,而这时,我们可以考虑使用 buffer 操作符,可以设置缓存的生成数,就像生产出的商品可以临时存储在仓库一样,并不会阻塞生产者的运行。

下面我们看下使用 buffer 前的效果:

fun dataSet() = flow {
    (1..3).forEach {
        delay(100)
        println(Thread.currentThread().name+"    flow")
        emit(it)
    }
}
    GlobalScope.launch {
        val time = measureTimeMillis {
            dataSet().collect { value ->
                delay(1000)
                println(Thread.currentThread().name+"    collect")
                println("value: $value")
            }
        }
        println("总耗时:$time")
    }

日志输出:

I/System.out: DefaultDispatcher-worker-1    flow
I/System.out: DefaultDispatcher-worker-1    collect
I/System.out: value: 1
I/System.out: DefaultDispatcher-worker-1    flow
I/System.out: DefaultDispatcher-worker-1    collect
I/System.out: value: 2
I/System.out: DefaultDispatcher-worker-1    flow
I/System.out: DefaultDispatcher-worker-1    collect
I/System.out: value: 3
I/System.out: 总耗时:4776

确实可以看出,是生产一个,然后消费一个,总耗时为:4776

加上 buffer 后:

    GlobalScope.launch {
        val time = measureTimeMillis {
            dataSet().buffer(10).collect { value ->
                delay(1000)
                println(Thread.currentThread().name+"    collect")
                println("value: $value")
            }
        }
        println("总耗时:$time")
    }
I/System.out: DefaultDispatcher-worker-1    flow
I/System.out: DefaultDispatcher-worker-2    flow
I/System.out: DefaultDispatcher-worker-2    collect
I/System.out: value: 1
I/System.out: DefaultDispatcher-worker-2    flow
I/System.out: DefaultDispatcher-worker-2    collect
I/System.out: value: 2
I/System.out: DefaultDispatcher-worker-2    collect
I/System.out: value: 3
I/System.out: 总耗时:3763

总耗时为:3763,比 4776 少了不少,并且可以看出,它还会自动切换线程进行执行。

conflate

合并发射内容。其实,它算是两个功能的合成,即一个是 buffer 效果,另外一个是合并效果。buffer 消息就不多解释了,刚介绍完,而合并效果就是,例如生产者生产了 A、B、C 三个货品到仓库中,消费者去拿的时候,之后拿首位和尾位,忽略中间的,所以,消费者实际消费只有 A、C,B 就被丢弃了。

我们可以看看具体的实例效果:

fun dataSet() = flow {
    (1..3).forEach {
        println("flow value: $it")
        emit(it)
    }
}
    GlobalScope.launch {
        dataSet().conflate().collect { value ->
            delay(1000)
            println("collect value: $value")
        }
    }

日志输出:

I/System.out: flow value: 1
I/System.out: flow value: 2
I/System.out: flow value: 3
I/System.out: collect value: 1
I/System.out: collect value: 3

collectLatest

collectLatest 的效果跟 conflate 类似,只不过改为获取最后一个数据。

    GlobalScope.launch {
        dataSet().collectLatest { value ->
            delay(1000)
            println("collect value: $value")
        }
    }
I/System.out: flow value: 1
I/System.out: flow value: 2
I/System.out: flow value: 3
I/System.out: collect value: 3

map

用于将原有的数据进行转换,再进行 collect 操作。

fun dataSet() = flow {
    (1..3).forEach {
        emit(it)
    }
}
    GlobalScope.launch {
        dataSet().map { request -> "map$request"}.collect { value ->
            println("value: $value")
        }
    }

日志输出:

I/System.out: value: map1
I/System.out: value: map2
I/System.out: value: map3

transform

map 操作符只能对于数据进行转换,无法对数据进行增删改,而 transform 则可以实现该功能。

fun dataSet() = flow {
    (1..3).forEach {
        emit(it)
    }
}
    GlobalScope.launch {
        dataSet().transform { data ->
            when(data){
                1 -> {
                    emit("data1")
                    emit("data11")
                }
                2 -> {}
                3 -> emit("data3")
            }
        }.collect {
            println("value: $it")
        }
    }
  • when 为 1 时,是增的情况
  • when 为 2 时,是删的情况
  • when 为 3 时,是改的情况

take

限定收集流的长度,也就是限制只收集多少个。

fun dataSet() = flow {
    (1..3).forEach {
        emit(it)
    }
}
    GlobalScope.launch {
        dataSet().take(2).collect {
            println("value: $it")
        }
    }

输出:

I/System.out: value: 1
I/System.out: value: 2

toList

将数据转换为 List。

toSet

将数据转换为 Set。

first

获取首位的值:

fun dataSet() = flow {
    (1..3).forEach {
        emit(it)
    }
}
    GlobalScope.launch {
        println("value: ${dataSet().first()}")
    }

日志输出:

I/System.out: value: 1

single

用于确保 flow 输出值唯一。若只有一个值,则可以正常执行,若输出的值不止只有一个的时候,就会抛出异常:

值不唯一:

    GlobalScope.launch {
        try {
            println("value: ${(1..3).asFlow().single()}")
        }catch (e: Exception){
            println("Exception: $e")
        }
    }

日志输出:

I/System.out: Exception: java.lang.IllegalArgumentException: Flow has more than one element

这里有一点要注意的,若不对该异常进行 try catch 的话,会引发整个 App 的崩溃,是真正抛出的异常。

值唯一:

    GlobalScope.launch {
        try {
            println("value: ${(1..1).asFlow().single()}")
        }catch (e: Exception){
            println("Exception: $e")
        }
    }

日志输出:

I/System.out: value: 1

reduce

对于 flow 里面的数据进行两个两个处理。例如 flow 有三个值,分别为 A、B、C,那么,会先处理 A 和 B,然后把 A he B 的结果再与 C 进行处理。

为了方便大家理解,这里使用两个🌰:

       val sum =  (1..3).asFlow().reduce { accumulator, value ->  accumulator + value}
        println("sum: $sum")

        val content = mutableListOf("one","two", "three").asFlow().reduce { accumulator, value -> "$accumulator  $value" }
        println("content: $content")

日志输出为:

I/System.out: sum: 6
I/System.out: content: one  two  three

在例一中

  • 最开始 accumulator 为 1,value 为 2,结果为 1+2=3
  • 而后 accumulator 为 3,value 为 3,结果为 3+3=6

fold

给定一个值,然后对 flow 的第一个值进行处理,然后把处理的结果再与第二值进行处理,以此类推。这里要注意的是,处理结果的类型需要与给定的值的类型保持一致。

em...太难懂了,还是看代码吧:

       val sum =  (1..3).asFlow().fold(3) { accumulator, value ->  accumulator * value}
        println("sum: $sum")

       val content =  (1..3).asFlow().fold("data") { accumulator, value ->  "$accumulator$value"}
        println("content: $content")

日志输出:

I/System.out: sum: 18
I/System.out: content: data123

在例一中

  • 最开始 accumulator 为 3,value 为 1,结果为 3*1=3
  • 而后 accumulator 为 3,value 为 2,结果为 3*2=6
  • 而后 accumulator 为 6,value 为 3,结果为 6*3=18

zip

将两个 flow 进行合并,把两两合并的结果再交由 collect 进行处理:

    GlobalScope.launch {
        val stringFlow = mutableListOf("one", "two", "three").asFlow()
        val numFlow = (1..3).asFlow()
        stringFlow.zip(numFlow) { string, num ->
            "$string$num"
        }.collect {
            println("value -> $it")
        }
    }
I/System.out: value -> one:1
I/System.out: value -> two:2
I/System.out: value -> three:3

不过,这里有一点需要注意的是,假如两个流的长度不一致,那就以最短的为准:

    GlobalScope.launch {
        val stringFlow = mutableListOf("one", "two").asFlow()  // 长度为 2
        val numFlow = (1..3).asFlow() // 长度为 3
        stringFlow.zip(numFlow) { string, num ->
            "$string$num"
        }.collect {
            println("value -> $it")
        }
    }
I/System.out: value -> one:1
I/System.out: value -> two:2

另外,还有一点,在这里,若两个流产生元素的速度不一致的时候,会等待两个流都产生一个元素后再进行处理:

    GlobalScope.launch {
        val stringFlow = mutableListOf("one", "two", "three").asFlow().onEach { delay(1000) }
        val numFlow = (1..3).asFlow().onEach { delay(1500) }
        stringFlow.zip(numFlow) { string, num ->
            "$string$num"
        }.collect {
            println("value -> $it")
        }
    }
I/System.out: value -> one:1
I/System.out: value -> two:2
I/System.out: value -> three:3

Combine

Combine 的功能跟 zip 十分相似,但是有一点不同,就是当流 A 产生一个元素 a 的时候,若流 B 还未产生新的元素 b,但之前有产生过元素 c,这时会先让 a 和 c 进行匹配,然后等 b 产生后,a 会和 b 再次进行匹配。同样,我们可以通过代码更方便去理解:

    GlobalScope.launch {
        val stringFlow = mutableListOf("one", "two", "three").asFlow().onEach { delay(1000) }
        val numFlow = (1..3).asFlow().onEach { delay(1500) }
        stringFlow.combine(numFlow) { string, num ->
            "$string$num"
        }.collect {
            println("value -> $it")
        }
    }
I/System.out: value -> one:1
I/System.out: value -> two:1
I/System.out: value -> two:2
I/System.out: value -> three:2
I/System.out: value -> three:3

flatMapConcat

map 的操作符是让元素 A 改为元素 B,而 flatMapConcat 的操作符是让元素 A 改为流 B。

    GlobalScope.launch {
        (1..3).asFlow().onEach { delay(100) }.flatMapConcat{ num ->
          flow {
              emit("num1: $num")
              delay(200)
              emit("num2: $num")
          }
        }.collect {
            println("value -> $it")
        }
    }

日志输出:

I/System.out: value -> num1: 1
I/System.out: value -> num2: 1
I/System.out: value -> num1: 2
I/System.out: value -> num2: 2
I/System.out: value -> num1: 3
I/System.out: value -> num2: 3

flatMapMerge

flatMapMerge 与 flatMapConcat 相似,不同的是,flatMapConcat 会等待自己每个元素执行完 flatMapConcat 的转换,才会进行下一个元素执行,而 flatMapMerge 不一样,它不会等待元素执行完,而是直接开始下一个元素执行 flatMapMerge 的转换。所以,就会看到输出的值有点乱的原因。

说白了,其实就是 元素 A 在执行 flatMapMerge 的转换,还未执行完,元素 B 也在执行 flatMapMerge 的转换。

    GlobalScope.launch {
        (1..3).asFlow().onEach { delay(100) }.flatMapMerge{ num ->
          flow {
              emit("num1: $num")
              delay(200)
              emit("num2: $num")
          }
        }.collect {
            println("value -> $it")
        }
    }
I/System.out: value -> num1: 1
I/System.out: value -> num1: 2
I/System.out: value -> num2: 1
I/System.out: value -> num1: 3
I/System.out: value -> num2: 2
I/System.out: value -> num2: 3

flatMapLatest

flatMapLatest 又与 flatMapMerge 类似,不同的是,当有新元素的 emit 的时候,旧元素的 emit 就会被丢弃:

    GlobalScope.launch {
        (1..3).asFlow().flatMapLatest{ num ->
          flow {
              emit("num1: $num")
              delay(200)
              emit("num2: $num")
          }
        }.collect {
            println("value -> $it")
        }
    }
I/System.out: value -> num1: 1
I/System.out: value -> num1: 2
I/System.out: value -> num1: 3
I/System.out: value -> num2: 3

catch

捕获流产生的异常。

    GlobalScope.launch {
        flow {
            throw NullPointerException()
            emit(1)
        }.catch { e ->
            println("已捕获异常 -> $e")
        }.collect { value ->
            println("value -> $value")
        }
    }
I/System.out: 已捕获异常 -> java.lang.NullPointerException

当然,也是可以使用 try catch 进行捕获的:

    GlobalScope.launch {
        val flow = flow {
            throw NullPointerException()
            emit(1)
        }

        try {
            flow.collect { value ->
                println("value -> $value")
            }
        } catch (e: Exception) {
            println("已捕获异常 -> $e")
        }
    }
I/System.out: 已捕获异常 -> java.lang.NullPointerException

onCompletion

用于流执行完成的回调。

    GlobalScope.launch {
        flow {
            emit(1)
        }.onCompletion {
            println("流执行完成了")
        }.collect { value ->
            println("value -> $value")
        }
    }
I/System.out: value -> 1
I/System.out: 流执行完成了

当然,也是可以使用 try finally 进行实现的:

    GlobalScope.launch {
        val flow = flow {
            emit(1)
        }
        try {
            flow.collect { value ->
                println("value -> $value")
            }
        } finally {
            println("流执行完成了")
        }
    }
}
I/System.out: value -> 1
I/System.out: 流执行完成了