Flow

204 阅读7分钟

认识 Flow

如何表示多个值?

挂起函数可以异步的返回单个值,但是该如何异步返回多个计算好的值呢?

首先来看看集合如何返回多个值:

fun main() {
    simpleList().forEach { value ->
        println(value)
    }
}
fun simpleList() : List<Int> = listOf(1, 2, 3)
//1
//2
//3

集合虽然返回了多个值,但是不是异步的。

我们来看下如何用序列返回多个值:

fun main() {
    simpleSequence().forEach { value ->
        println(value)
    }
}
fun simpleSequence() : Sequence<Int> = sequence {
    for (i in 1..3) {
        Thread.sleep(1000)
        yield(i)
    }
}
//1
//2
//3

在这个例子里我们延时1000ms来假装在计算,之后输出值,但是这里是线程阻塞的,并不是异步的。而且这里并不能使用 delay() 函数,受 RestrictsSuspension 注解的约束,delay 不能在 SequenceScope 的扩展成员当中被调用,因而不能在序列生成器的协程体内调用了。

如果我们使用集合➕挂起函数呢?

suspend fun main() {
    simpleList2().forEach { value ->
        println(value)
    }
}
 
suspend fun simpleList2() : List<Int> {
    delay(1000)
    return listOf(1, 2, 3)
}
//1
//2
//3

这里虽然返回了多个值,也是异步,但是是一次性返回了多个值。那如何一次只给一个值呢? 我们来看看使用 Flow 的方式:

suspend fun main() {
    simpleFlow().collect { value ->
        println(value)
    }
}
fun simpleFlow() = flow<Int> {
    for (i in 1..3) {
        delay(1000)
        emit(i) // 发射,产生一个元素
    }
}
//1
//2
//3

这里我们每隔1s拿到一个元素,和 sequence 很像,但是这里是异步的,sequence 是阻塞的。

这里总结一下:

  • 名为 flow 的 Flow 类型构建器函数。
  • flow{...} 构建块中的代码可以挂起。
  • 函数 simpleFlow 不再标有 suspend 修饰符。
  • 流使用 emit 函数发射值。
  • 流使用 collect 函数收集值。

冷数据流

Flow 是一种类似于序列的冷数据流,flow 构建器中的代码直到流被收集的时候菜运行,不消费则不生产,多次消费则多次生产,生产和消费总是相对应的。

suspend fun main() {
    val flow = simpleFlow2()
    println("Calling collect...")
    flow.collect { value -> println(value) }
    println("Calling collect again...")
    flow.collect { value -> println(value) }
 
}
 
fun simpleFlow2() = flow<Int> {
    println("Flow started")
    for (i in 1..3) {
        kotlinx.coroutines.delay(1000)
        emit(i)
    }
}
//Calling collect...
//Flow started
//1
//2
//3
//Calling collect again...
//Flow started
//1
//2
//3

Flow 的连续性

流点的每次单独收集都是按顺序执行的,除非使用特殊操作符。

从上游到下游每个过渡操作符都会处理每隔发射出的值,然后再交给末端操作符。

suspend fun main() {
    (1..5).asFlow().filter {
        it % 2 == 0
    }.map {
        "string $it"
    }.collect {
        println("Collect $it")
    }
}
//Collect string 2
//Collect string 4

Flow 的构建器

flowOf 构建器定义了一个发射固定值集的流。

下面每隔1s输出一个值:

suspend fun main() {   
    flowOf("one", "two", "three")
        .onEach { delay(1000) }
        .collect {
            println(it)
        }
}
//one
//two
//three

使用 .asFlow() 扩展函数,可以将各种集合与序列转换为流。

suspend fun main() {
    (1..3).asFlow().collect {
        println(it)
    }
}
//1
//2
//3

Flow 的上下文

流的收集总是在调用协程的上下文中发生,流的该属性称为上下文保存

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

flowOn操作符,该函数用于更改流发射的上下文。

suspend fun main() {
    simpleFlow3().collect {
        println("Collected $it ${Thread.currentThread().name}")
    }
}
fun simpleFlow3() = flow<Int> {
    println("Flow started ${Thread.currentThread().name}")
    for (i in 1..3) {
        kotlinx.coroutines.delay(1000)
        emit(i)
    }
}.flowOn(Dispatchers.Default)
//Flow started DefaultDispatcher-worker-1
//Collected 1 DefaultDispatcher-worker-1
//Collected 2 DefaultDispatcher-worker-1
//Collected 3 DefaultDispatcher-worker-1

在指定协程收集流

使用 launchIn 替换 collect 我们可以在单独的协程中启动流的收集。

suspend fun main() {
    events().onEach { event -> println("Event: $event ${Thread.currentThread().name}") }
        .launchIn(CoroutineScope(Dispatchers.IO))
        .join()
}
 
fun events() = (1..3)
    .asFlow()
    .onEach { delay(1000) }
    .flowOn(Dispatchers.Default)
 
//Event: 1 DefaultDispatcher-worker-1
//Event: 2 DefaultDispatcher-worker-2
//Event: 3 DefaultDispatcher-worker-1

Flow 的取消

流采用与协程同样的协作取消,流的取消可以是当流在一个可取消的挂起函数(例如 delay)中挂起的时候取消,Flow 的消费依赖于 collect 这样的末端操作符,而它们又必须在协程当中调用,因此 Flow 的取消主要依赖于末端操作符所在的协程的状态。

suspend fun main() {
    withTimeoutOrNull(2500) {
        simpleFlow4().collect { println(it) }
    }
    println("Done")
}
 
fun simpleFlow4() = flow<Int> {
    for (i in 1..3) {
        delay(1000)
        emit(i)
        println("Emitting $i")
    }
}
//1
//Emitting 1
//2
//Emitting 2
//Done

Flow 的取消检测

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

fun main() = runBlocking {
    simpleFlow5().collect {
        println(it)
        if (it == 3) cancel()
    }
}
 
fun simpleFlow5() = flow<Int> {
    for (i in 1..5) {
        emit(i)
        println("Emitting $i")
    }
}
//1
//Emitting 1
//2
//Emitting 2
//3
//Emitting 3
//Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@3d121db3

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

fun main() = runBlocking {
    (1..5).asFlow().collect {
        println(it)
        if (it == 3) cancel()
    }
}
//1
//2
//3
//4
//5
//Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@6e75aa0d

通过 cancellable 操作符来执行此操作。

fun main() = runBlocking {
    (1..5).asFlow().cancellable().collect {
        println(it)
        if (it == 3) cancel()
    }
}
//1
//2
//3
//Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@6e75aa0d

Flow 的背压

背压问题在生产者的生产速率高于消费者的处理速率的情况下出现。那我们如何处理背压问题呢?

buffer(),并发运行流中发射元素的代码。

fun main() = runBlocking {
    val time = measureTimeMillis {
        simpleFlow6().collect {
            delay(300)
            println("Collected $it ${Thread.currentThread().name}")
        }
    }
    println("Collected in $time ms")
}
 
fun simpleFlow6() = flow<Int> {
    for (i in 1..3) {
        delay(100)
        emit(i)
        println("Emitting $i ${Thread.currentThread().name}")
    }
}
//Collected 1 main
//Emitting 1 main
//Collected 2 main
//Emitting 2 main
//Collected 3 main
//Emitting 3 main
//Collected in 1268 ms

发送需要100ms,接收需要300ms,处理三个数据一共花费1268ms,我们可以使用buffer()进行优化:

simpleFlow6()
    .buffer(50)
    .collect {
    delay(300)
    println("Collected $it ${Thread.currentThread().name}")
    }
//Emitting 1 main
//Emitting 2 main
//Emitting 3 main
//Collected 1 main
//Collected 2 main
//Collected 3 main
//Collected in 1094 ms

使用 buffer() 之后,节省了200ms,而且发送是会一次性发送完,相当于一个缓存,但这里都是在 mian线程执行,并不是并行,那如何并行呢?可以用 flowOn() 来实现:

simpleFlow6()
    .flowOn(Dispatchers.Default)
    //.buffer(50)
    .collect {
    delay(300)
    println("Collected $it ${Thread.currentThread().name}")
    }
//Emitting 1 DefaultDispatcher-worker-1
//Emitting 2 DefaultDispatcher-worker-1
//Emitting 3 DefaultDispatcher-worker-1
//Collected 1 main
//Collected 2 main
//Collected 3 main
//Collected in 1088 ms

conflate(),合并发射项,不对每个值进行处理。

simpleFlow6()
    .conflate()
    //.flowOn(Dispatchers.Default)
    //.buffer(50)
    .collect {
    delay(300)
    println("Collected $it ${Thread.currentThread().name}")
}
//Emitting 1 main
//Emitting 2 main
//Emitting 3 main
//Collected 1 main
//Collected 3 main
//Collected in 793 ms

使用 conflate(),发射了三个元素,但是第二个元素接收的时候过滤掉了。

collectLatest(),取消并重新发射最后一个值。

simpleFlow6()
    .collectLatest {
        delay(300)
        println("Collected $it ${Thread.currentThread().name}")
    }
//Emitting 1 main
//Emitting 2 main
//Emitting 3 main
//Collected 3 main
//Collected in 675 ms

操作符

转换操作符

map操作符:进行数据转换操作,包括转换发射出去的数据的类型

fun main() = runBlocking {
    (1..3).asFlow()
        .map { request -> performRequest(request) }
        .collect { println(it) }
}
suspend fun performRequest(request: Int): String {
    delay(1000)
    return "response $request"
}
//response 1
//response 2
//response 3

transform操作符: 操作任意值任意次,其他转换操作符都是基于transform进行扩展的。

(1..3).asFlow()
    .transform { request ->
        emit("Making request $request")
        emit(performRequest(request))
    }.collect { println(it) }
//Making request 1
//response 1
//Making request 2
//response 2
//Making request 3
//response 3

限长操作符

take操作符:take操作符返回包含第一个计数元素的流。

fun main() = runBlocking {
    numbers().take(2).collect { println(it) }
}
fun numbers() = flow<Int> {
    try {
        emit(1)
        emit(2)
        println("This line will not execute")
        emit(3)
    } finally {
        println("Finally in numbers")
    }
}
//1
//2
//Finally in numbers

末端流操作符

末端操作符是在流上用于启动流收集的挂起函数。collect 是最基础的末端操作符,但是还有另外一些更方便使用的末端操作符;

  • 转化为各种集合,例如 toList 和 toSet。
  • 获取第一个 (first) 值与确保流发射单个 (single) 值的操作符。
  • 使用 reduce 与 fold 将流规约到单个值。

下面给一个例子:

fun main() = runBlocking {
    val sum = (1..5).asFlow()
        .map { it * it }
        .reduce { a, b -> a + b }
    println(sum)
}
//55

组合操作符

zip操作符: zip操作符用于组合两个流中的相关值。

fun main() = runBlocking {
    val nums = (1..3).asFlow().onEach { delay(300) }
    val strs = flowOf("One", "Two", "Three").onEach { delay(400) }
    val startTime = System.currentTimeMillis()
    nums.zip(strs) { a, b -> "$a -> $b" }.collect {
        println("$it at ${System.currentTimeMillis() - startTime}ms from start")
    }
}
//1 -> One at 424ms from start
//2 -> Two at 825ms from start
//3 -> Three at 1233ms from start

展平操作符

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

flatMapConcat 连接模式

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    (1..3).asFlow()
        .onEach { delay(100) }
        .flatMapConcat { requestFlow(it) }
        .collect { println("$it at ${System.currentTimeMillis() - startTime}ms from start") }
}
fun requestFlow(i: Int) = flow<String> {
    emit("$i: First")
    delay(500)
    emit("$i: Second")
}
//1: First at 136ms from start
//1: Second at 637ms from start
//2: First at 753ms from start
//2: Second at 1259ms from start
//3: First at 1366ms from start
//3: Second at 1872ms from start

flatMapMerge 合并模式

(1..3).asFlow()
  .onEach { delay(100) }
  .flatMapConcat { requestFlow(it) }
  .collect { println("$it at ${System.currentTimeMillis() - startTime}ms from start") }
 
//1: First at 158ms from start
//2: First at 260ms from start
//3: First at 368ms from start
//1: Second at 664ms from start
//2: Second at 765ms from start
//3: Second at 875ms from start

flatMapLatest 最新展平模式

(1..3).asFlow()
    .onEach { delay(100) }
    .flatMapLatest { requestFlow(it) }
    .collect { println("$it at ${System.currentTimeMillis() - startTime}ms from start") }
 
//1: First at 164ms from start
//2: First at 274ms from start
//3: First at 380ms from start
//3: Second at 881ms from start

流的异常处理

当运算符中的发射器或代码抛出异常时,有几种处理异常的方法:

1. try/catch块
2. catch函数

fun main() = runBlocking {
    try {
        simpleFlow().collect {
            println(it)
            check(it <= 1) { "Collected $it"}
        }
    } catch (e: Throwable) {
        println("Catch $e")
    }
    flow {
        emit(1)
        throw ArithmeticException("Div 0")
    }.catch { e: Throwable ->
        println("Catch $e")
        emit(10)
    }.flowOn(Dispatchers.IO)
        .collect { println(it) }
}
fun simpleFlow() = flow<Int> {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i)
    }
}
//Emitting 1
//1
//Emitting 2
//2
//Catch java.lang.IllegalStateException: Collected 2
//Catch java.lang.ArithmeticException: Div 0
//1
//10

流的完成

当流收集完成时(普通情况或异常情况),它可能需要执行一个动作。

命令式 finally 块:

fun main() = runBlocking {
    try {
        simpleFlow().collect { println(it) }
    } finally {
        println("Done")
    }
}
fun simpleFlow() = (1..3).asFlow()
//1
//2
//3
//Done

onCompletion 声明式处理:

fun main() = runBlocking {
    simpleFlow()
        .onCompletion { e ->
            if (e != null) println("Flow completed exception")
        }.catch { e ->
            println("Catch $e")
        }.collect {
            println(it)
        }
}
fun simpleFlow() = flow<Int> {
    emit(1)
    throw RuntimeException()
}
//1
//Flow completed exception
//Catch java.lang.RuntimeException

Flow 实现多路复用

多数情况下,我们可以通过构造合适的 Flow 来实现多路复用的效果。

fun main() = runBlocking<Unit> {
    val name = "guest"
    coroutineScope {
        listOf(::getUserFromLocal, ::getUserFromRemote)
            .map { function ->
                function.call(name)
            }
            .map { deferred ->
                flow { emit(deferred.await()) }
            }.merge().collect { user -> println(user) }
    }
}
fun CoroutineScope.getUserFromLocal(name: String) = async(Dispatchers.IO) {
    delay(1000)
    "getUserFromLocal"
}
fun CoroutineScope.getUserFromRemote(name: String) = async(Dispatchers.IO) {
    delay(2000)
    "getUserFromRemote"
}
//getUserFromLocal
//getUserFromRemote

StateFlow 和 ShareFlow

可以参考郭神的文章:

guolin.blog.csdn.net/article/det…