Kotlin - 协程 - Flow

在Kotlin普及之前,RxJava无疑是Android开发领域中最受欢迎的响应式编程的三方库,而RxJava在我们日常的Android开发应用的最多的场景就是配合Retrofit进行网络请求和类似EventBus的事件订阅(RxBus)。但是到了2017年,随着LiveData刚一面世,就受到了很大的关注,LiveData是一个以观察者模式为核心,让界面对变量进行订阅,从而实现自动通知刷新的组件。跟一般的订阅比起来,LiveData有两大特点:一是他的目标非常直接,直指界面刷新,所以它的数据更新只发生在主线程。二是它借助了另外一个组件Lifecycle的功能,让它可以只在界面到了前台的时候才通知更新,避免了浪费性能。并且LiveData相比RxJava来说也有两大优点:

  • LiveData的学习成本比较低
  • LiveData相比较于RxJava要轻量级很多

所以在一些简单场景人们逐渐从RxJava过渡到LiveData,而一些比较复杂的场景还是使用RxJava,因为LiveData的轻量级也决定了它不够强大,不适合一些复杂场景。而随着Kotlin协程库的更新,Flow诞生了。

Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库。从文档的介绍来看Flow有点类似 RxJava,都是一种基于事件的响应式编程。那么接下来我们就看一下Flow的基本使用。

1.创建Flow

fun simpleFlow(): Flow = flow { // flow builder

    for (i in 1..3) {

        delay(100) // pretend we are doing something useful here

        emit(i) // emit next value

    }

}

 

fun main() = runBlocking {

    // Collect the flow

    simpleFlow().collect { value -> println(value) } 

}

通过上面例子可以看到,Flow有以下特征:

  • 可以用flow{ ... } 构建一个Flow类型

  • flow { ... }内可以使用suspend函数.

  • simpleFlow()不需要是suspend函数

  • emit方法用来发射数据

  • collect方法用来遍历结果

2.Flow是冷流

Flow是一种冷流,Flow构建器中的代码只有在collect函数被执行的时候才会运行。这一点与 Channel 正对应:Channel 的发送端并不依赖于接收端。

fun simpleFlow2() = flow {

    println("Flow started")

    for (i in 1..3) {

        delay(1000)

        emit(i)

    }

}



@Test

fun `test flow is cold`() = runBlocking {

    val flow = simpleFlow2()

    println("Calling collect...")

    flow.collect { value -> println(value) }

    println("Calling collect again...")

    flow.collect { value -> println(value) }

}

3.Flow是具有连续性的流

流的每次单独收集都是按顺序执行的,除非使用了特殊的操作符,从上游到下游每个过渡操作符都会处理每个发射出的值,然后再交给末端操作符

@Test

fun `test flow continuation`() = runBlocking {

    (1..5).asFlow().filter {

        it % 2 == 0

    }.map {

        "string $it"

    }.collect {

        println("Collect $it")

    }

}

4.Flow的构建器

通常情况下有两种方式可以构建一个Flow,一种是通过flowOf构建器定义一个发射固定值集的流

flowOf("one","two","three")

        .onEach { delay(1000) }

        .collect { value ->

            println(value)

        }

另一种方式是使用.asFlow()扩展函数可以将各种集合与序列转换为Flow

(1..3).asFlow().collect { value ->

    println(value)

}

5.Flow的上下文

  1. Flow的收集总是在调用协程的上下文中发生的,Flow的该属性称为上下文保存
fun simpleFlow3() = flow {

    println("Flow started ${Thread.currentThread().name}")

    for (i in 1..3) {

        delay(1000)

        emit(i)

    }

}



@Test

fun `test flow context`() = runBlocking {

    simpleFlow3()

            .collect { value -> println("Collected $value ${Thread.currentThread().name}") }

}
  1. flow{...}构建器中的代码必须遵循上下文保存属性,并且不允许从其他上下文中发射(emit)
fun simpleFlow4() = flow {

    withContext(Dispatchers.IO) {

        println("Flow started ${Thread.currentThread().name}")

        for (i in 1..3) {

            delay(1000)

            emit(i)

        }

    }

} //Error
  1. flowOn操作符用于更改流发射的上下文
fun simpleFlow5() = flow {

    println("Flow started ${Thread.currentThread().name}")

    for (i in 1..3) {

        delay(1000)

        emit(i)

    }

}.flowOn(Dispatchers.Default)

6.分离 Flow 的消费和触发

我们除了可以在 collect 处消费 Flow 的元素以外,还可以通过 onEach 来做到这一点。这样消费的具体操作就不需要与末端操作符放到一起,collect 函数可以放到其他任意位置调用,例如:

fun createFlow() = flow {

    (1..3).forEach {

      emit(it)

      delay(100)

    }

  }.onEach { println(it) }



fun main(){

  GlobalScope.launch {

    createFlow().collect()

  }

}

7.Flow的取消

Flow本身并没有提供取消操作, Flow 的消费依赖于 collect 这样的末端操作符,而它们又必须在协程当中调用,因此 Flow的取消主要依赖于末端操作符所在的协程的状态。像往常一样,Flow的收集可以是当流在一个可取消的挂起函数中取消的时候取消。

fun simpleFlow6() = flow {

    for (i in 1..3) {

        delay(1000)

        emit(i)

        println("Emitting $i")

    }

}





@Test

fun `test cancel flow`() = runBlocking {

    withTimeoutOrNull(2500) {

        simpleFlow6().collect { value -> println(value) }

    }

    println("Done")

}

8.Flow的取消检测

  • 为方便起见,流构建器对每个发射值执行附加的enureActive检测以进行取消,这意味着从flow{...}发出的繁忙循环是可以取消的
fun simpleFlow7() = flow {

    for (i in 1..5) {

        emit(i)

        println("Emitting $i")

    }

}



@Test

fun `test cancel flow check`() = runBlocking {

    simpleFlow7().collect { value ->

        println(value)

        if (value == 3) cancel()

    }

}
  • 出于性能原因,大多数其他流操作不会自行执行其他取消检测,在协程出于繁忙循环的情况下,必须明确检测是否取消。
@Test

fun `test cancel flow check`() = runBlocking {

   (1..5).asFlow().collect { value ->

        println(value)

        if (value == 3) cancel()

    }

}
  • 一般使用cancellable操作符来执行此操作
@Test

fun `test cancel flow check`() = runBlocking {

   (1..5).asFlow().cancellable().collect { value ->

        println(value)

        if (value == 3) cancel()

    }

}

9.Flow的背压

只要是响应式编程,就一定会有背压问题,生产者的生产速率高于消费者的处理速率的情况下出现。为了保证数据不丢失,我们也会考虑添加缓存来缓解问题,buffer的本质是并发运行流中发射元素的代码

fun simpleFlow8() = flow {

    for (i in 1..3) {

        delay(100)

        emit(i)

        println("Emitting $i ${Thread.currentThread().name}")

    }

}



@Test

fun `test flow back pressure`() = runBlocking {

    val time = measureTimeMillis {

        simpleFlow8()

               .buffer(50)

               .collect { value ->

            delay(300)   

            println("Collected $value ${Thread.currentThread().name}")

        }

    } 

    println("Collected in $time ms") //1028ms

}

不过,如果我们只是单纯地添加缓存,而不是从根本上解决问题就始终会造成数据积压。问题产生的根本原因是生产和消费速率的不匹配,除直接优化消费者的性能以外,我们也可以采取一些取舍的手段。第一种是 conflate,conflate()的策略是如果缓存池满了,新数据会覆盖老数据

fun simpleFlow8() = flow {

    for (i in 1..3) {

        delay(100)

        emit(i)

        println("Emitting $i ${Thread.currentThread().name}")

    }

}



@Test

fun `test flow back pressure`() = runBlocking {

    val time = measureTimeMillis {

        simpleFlow8() 

                .conflate()

                .collect { value ->

            println("Collected start $value ${Thread.currentThread().name}")    

            delay(300)   

            println("Collected end $value ${Thread.currentThread().name}")

        }

    }



    println("Collected in $time ms") //770ms

}

第二种是 collectLatest。顾名思义,只处理最新的数据,这看上去似乎与 conflate 没有区别,其实区别大了:它并不会直接用新数据覆盖老数据,而是每一个都会被处理,只不过如果前一个还没被处理完后一个就来了的话,处理前一个数据的逻辑就会被取消。

fun simpleFlow8() = flow {

    for (i in 1..3) {

        delay(100)

        emit(i)

        println("Emitting $i ${Thread.currentThread().name}")

    }

}



@Test

fun `test flow back pressure`() = runBlocking {

    val time = measureTimeMillis {

        simpleFlow8()

            .collectLatest { value ->

            println("Collected start $value ${Thread.currentThread().name}")  

            delay(300)  

            println("Collected $value ${Thread.currentThread().name}")

        }

    }

    println("Collected in $time ms")//785ms

}

collectLatest 之外还有 mapLatestflatMapLatest 等等,都是这个作用。

10.Flow的操作符

  1. 转换操作符(过渡操作符)
  • 可以使用操作符转换流,就像使用集合与序列一样
  • 过渡操作符应用于上游流,并返回下游流
  • 这些操作符也是冷操作符,并且这类操作符本身并不是挂起函数
  • 运行速度很快,返回新的转换流的定义
suspend fun performRequest(request: Int): String {

    delay(1000)

    return "response $request"

}



@Test

fun `test transform flow operator1`() = runBlocking {

    (1..3).asFlow()

            .map { request -> performRequest(request) }

            .collect { value -> println(value) }

}





@Test

fun `test transform flow operator2`() = runBlocking {

    (1..3).asFlow()

            .transform { request ->

                emit("Making request $request")

                emit(performRequest(request))

            }.collect { value -> println(value) }



}
  1. 限长操作符
fun numbers() = flow { 

    emit(1)

    emit(2)

    emit(3)

}



@Test

fun `test limit length operator`() = runBlocking {

    numbers().take(2).collect { value -> println(value) }

}
  1. 末端操作符

末端操作符是在流上用于启动流收集的挂起函数。collect是最基础的末端操作符,功能与 RxJava 的 subscribe 类似。但还有另外一些更方便使用的末端操作符,大体分为两类:

  • 集合类型转换操作,包括 toListtoSet 等。
  • 聚合操作,包括将 Flow 规约到单值的 reducefold 等操作,以及获得单个元素的操作包括 singlesingleOrNullfirst 等。

实际上,识别是否为末端操作符,还有一个简单方法,由于 Flow 的消费端一定需要运行在协程当中,因此末端操作符都是挂起函数。

@Test

fun `test terminal operator`() = runBlocking {

    val sum = (1..5).asFlow()

            .map { it * it }

            .reduce { a, b -> a + b }

    println(sum)

}
  1. 组合操作符

就像Kotlin标准库中的Sequence.zip()扩展函数一样,可以使用zip操作符组合两个流中的相关值

@Test

fun `test zip`() = runBlocking {

    val numbs = (1..3).asFlow()

    val strs = flowOf("One", "Two", "Three")

    numbs.zip(strs) { a, b -> "$a -> $b" }.collect { println(it) }

}

combine 虽然也是合并,但是跟 zip 不太一样。

使用 combine 合并时,每次从 flowA 发出新的 item ,会将其与 flowB 的最新的 item 合并。

fun main() = runBlocking {

    val flowA = (1..5).asFlow().onEach { delay(100)  }

    val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200)  }

    flowA.combine(flowB) { a, b -> "$a and $b" }

        .collect { println(it) }

}
1 and one

2 and one

3 and one

3 and two

4 and two

5 and two

5 and three

5 and four

5 and five
  1. 展平操作符

Flow表示异步接收的值序列,所以很容易遇到这样的情况:每个值都会触发对另一个值序列的请求。然而由于流具有“异步”的性质,因此需要不同的展平模式,为此,存在一系列的展平操作符:

  1. flatMapConcat
  2. flatMapMerge
  3. flatMapLatest
fun requestFlow(i: Int) = flow {

    emit("$i: First")

    delay(500)

    emit("$i: Second")

}



@Test

fun `test map`() = runBlocking {

    //Flow>

    val startTime = System.currentTimeMillis()

    (1..3).asFlow()

            .map { requestFlow(it) }

            .collect { println("$it at ${System.currentTimeMillis() - startTime} ms from start") }

}



@Test

fun `test flatMapConcat`() = runBlocking {

    val startTime = System.currentTimeMillis()

    (1..3).asFlow()

            .onEach { delay(100) }

            .flatMapConcat { requestFlow(it) }

            .collect { println("$it at ${System.currentTimeMillis() - startTime} ms from start") }

}



1: First

1: Second

2: First

2: Second

3: First

3: Second



@Test

fun `test flatMapMerge`() = runBlocking {

    val startTime = System.currentTimeMillis()

    (1..3).asFlow()

            .onEach { delay(100) }

            .flatMapMerge { requestFlow(it) }

            .collect { println("$it at ${System.currentTimeMillis() - startTime} ms from start") }

}



1: First

2: First

3: First

1: Second

2: Second

3: Second



@Test

fun `test flatMapLatest`() = runBlocking {

    val startTime = System.currentTimeMillis()

    (1..3).asFlow()

            .onEach { delay(100) }

            .flatMapLatest { requestFlow(it) }

            .collect { println("$it at ${System.currentTimeMillis() - startTime} ms from start") }

}



1: First

2: First

3: First

3: Second

11.Flow的异常处理

当运算符中的发射器或者代码抛出异常时,通常有一下两个处理方法:

第一种是 try/catch代码块

fun simpleFlow() = flow {

    for (i in 1..3) {

        println("Emitting $i")

        emit(i)

    }

}



@Test

fun `test flow exception`() = runBlocking {

    try {

        simpleFlow().collect { value ->

            println(value)

            check(value <= 1) { "Collected $value" }

        }

    } catch (e: Throwable) {

        println("Caught $e")

    }

}

第二种是通过catch()函数,但是只能捕获上游异常

@Test

fun `test flow exception2`() = runBlocking {

    flow {

        emit(1)

        throw ArithmeticException("Div 0")

    }.catch { e: Throwable -> println("Caught $e") }

        .flowOn(Dispatchers.IO)

        .collect { println(it) }



}

异常恢复

@Test

fun `test flow exception2`() = runBlocking {

    flow {

        throw ArithmeticException("Div 0")

        emit(1)

    }.catch { e: Throwable ->

        println("Caught $e")

        emit(10)

    }.collect { println(it) }

}

12.Flow的完成

当流收集完成时,它可能需要执行一个动作

  1. 命令式finally代码块
  2. onCompletion声明式处理,onCompletion 用起来比较类似于 try ... catch ... finally 中的 finally,无论前面是否存在异常,它都会被调用,参数 t 则是前面未捕获的异常。
flow {

  emit(1)

  throw ArithmeticException("Div 0")

}.catch { t: Throwable ->

  println("caught error: $t")

}.onCompletion { t: Throwable? ->

  println("finally.")

}

你可能感兴趣的:(Kotlin - 协程 - Flow)