【Kotlin回顾】17.Kotlin协程—Flow

248 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情

1.认识Flow上游

先看一下Flow的简单使用

fun flowTest() = runBlocking {
    flow {
        emit(0)
        emit(1)
        emit(2)
        emit(3)
        emit(4)
    }.collect {
        println("it:$it")
    }
}

//输出结果:
//it:0
//it:1
//it:2
//it:3
//it:4

Flow从字面意思理解就是流,Flow除了有发送方和接收方之外还可以有中转站,什么是中转站呢,例如水流,水流从源头汇入大海中间会经过水库、支流等。

Flow的中转站用法

fun flowTest() = runBlocking {
    flow {
        emit(0)
        emit(1)
        emit(2)
        emit(3)
        emit(4)
    }.filter {			//中转站①
        it > 2
    }.map {				//中转站②
        it * 2
    }.collect {			//接收
        println("it:$it")
    }
}

//输出结果:
//it:6
//it:8

对上面的代码进行逐个分析:

  • flow{}: 是一个高阶函数,作用就是创建一个新的Flow,创建好后就要把消息发送出去,这里的emit是发射、发送的意思,那么flow{}的作用就是创建一个数据流并且将数据发送出去;
  • filter{}、map{}: 这是中间操作符,都是高阶函数,就像中转站一样对数据进行处理后向下传递;
  • collect{}: 终止操作符,终止Flow数据流并接收从上游传递的数据。

除了通过flow{}创建Flow之外还有flowOf{},也可以创建一个Flow

fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            it > 2
        }.map {
            it * 2
        }.collect {
            println("it:$it")
        }
}

//输出结果:
//it:6
//it:8

上面通过两段代码实现了Flow的创建,这时候两有个疑问:

1.collect是终止操作符,作用是接收从上游传递的数据,那要是不接收会怎么样?
fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            println("filter:$it")
            it > 2
        }
        .map {
            println("map:$it")
            it * 2
        }
}

//没有输出任何日志程序就结束了

运行上面的代码会发现什么都没有做就结束了,而添加collect函数后filtermap的日志就是正常输出的,因此得出一个结论:只有调用终止操作符collect之后,Flow 才会开始工作。

已知Channel是“热”的,它不管有没有接收方,发送方都会工作, 那么可以总结出Flow就是冷的,Flow没有接收方是不会开始工作的。

2.两段代码都发送了5条数据,然后由collect接收,那么是一次发送完毕还是逐条发送呢?
fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            println("filter:$it")
            it > 2
        }.map {
            println("map:$it")
            it * 2
        }.collect {
            println("it:$it")
        }
}

//输出结果:
//filter:0
//filter:1
//filter:2
//filter:3
//map:3
//it:6
//filter:4
//map:4
//it:8

从输出结果可以很清楚的知道Flow一次只会处理一条数据。

上面是通过Flow的API创建一个流,但是还有一个更神奇的方式也可以实现近似于flowOf的效果

fun flowTest() = runBlocking {
//区别在这里
//    ↓
    listOf(0, 1, 2, 3, 4)
        .filter {
            it > 2
        }.map {
            it * 2
        }.forEach() {			//区别在这里
            println("it:$it")
        }
}

//输出结果:
//it:6
//it:8

flowOflistOf的代码对比之后可以看到 Flow API 与集合 API 之间的共性。listOf 创建 List,遍历使用 forEach{}flowOf 创建 Flow,遍历 Flow使用 collect{}

Kotlin还提供了Flow转List、List转Flow的API

fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .toList()					//Flow转List
        .filter {
            it > 2
        }.map {
            it * 2
        }.forEach {					//collect变为forEach
            println("it:$it")
        }
 
    listOf(0, 1, 2, 3, 4)
        .asFlow()					//List转
        .filter {
            it > 2
        }.map {
            it * 2
        }.collect {					//forEach变为collect
            println("it:$it")
        }
}

//输出结果:
//it:6
//it:8
//it:6
//it:8

flowflowOflistOf否尅创建Flow,那么他们有什么区别?

Flow创建方式使用场景用法
flow未知数据集flow{ emit() }.collect{ }
flowOf已知数据集flowOf(T).collect{ }
listOf已知数据来源的集合listOf(T).asFlow().collect{ }

2.认识Flow中转站

Flow中转站指的就是中间操作符:

1.与集合一样的操作符,这里只是摘抄了部分操作符

/**
 * 返回只包含与给定[predicate]匹配的原始流的值的流
 */
public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value ->
    if (predicate(value)) return@transform emit(value)
}

/**
 * 返回只包含与给定[predicate]值不匹配的原始流的值的流
 */
public inline fun <T> Flow<T>.filterNot(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value ->
    if (!predicate(value)) return@transform emit(value)
}

/**
 * 返回一个只包含原始流的非空值的流
 */
public fun <T: Any> Flow<T?>.filterNotNull(): Flow<T> = transform<T?, T> { value ->
    if (value != null) return@transform emit(value)
}

/**
 * 返回一个流,其中包含对原始流的每个值应用给定[transform]函数的结果。
 */
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
    return@transform emit(transform(value))
}

/**
 * 返回一个流,将每个元素包装成[IndexedValue],包含value和它的索引(从0开始)。
 */
public fun <T> Flow<T>.withIndex(): Flow<IndexedValue<T>> = flow {
    var index = 0
    collect { value ->
        emit(IndexedValue(checkIndexOverflow(index++), value))
    }
}

/**
 * 返回一个流,在上游流的每个值被下游发出之前调用给定的[action]。
 */
public fun <T> Flow<T>.onEach(action: suspend (T) -> Unit): Flow<T> = transform { value ->
    action(value)
    return@transform emit(value)
}

2.Flow特有的操作符——Flow生命周期

/**
 * 返回在开始收集此流之前调用给定操作的流。
 * 该操作在上游流启动之前被调用,因此如果它与SharedFlow一起使用,
 * 则不能保证上游流内部或onStart操作之后发生的排放会被收集
 */
public fun <T> Flow<T>.onStart(
    action: suspend FlowCollector<T>.() -> Unit
): Flow<T> = unsafeFlow { 
   ...
}
fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            println("filter")
            it > 2
        }.map {
            println("map")
            it * 2
        }.onStart {
            println("onStart")
        }.collect {
            println("it:$it")
        }
}

//输出结果:
//onStart
//filter
//filter
//filter
//filter
//map
//it:6
//filter
//map
//it:8

可以看到onStart函数的执行数序与它在代码中定义的顺序没有关系,而其他两个操作符filtermap的执行流程则跟它们定义的顺序息息相关。

/**
 * 返回一个流,该流在流完成或取消后调用给定的操作,传递取消异常或失败作为操作的原因参数。
 * 从概念上讲,onCompletion类似于将流集合包装到finally块中,例如下面的命令代码片段:
 */
public fun <T> Flow<T>.onCompletion(
    action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit
): Flow<T> = unsafeFlow {
    ...
}
fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            println("filter")
            it > 2
        }.map {
            println("map")
            it * 2
        }.onCompletion {
            println("onCompletion")
        }.collect {
            println("it:$it")
        }
}

//输出结果:
//filter
//filter
//filter
//filter
//map
//it:6
//filter
//map
//it:8
//onCompletion

onCompletion在它的注释中也标注的比较清楚,类似于finally,都是在最后执行。

3.Flow特有的操作符——catch异常处理

Flow中的catch异常处理时要遵循上下游规则的,因为Flow是具有上下游之分的,具体来讲就是catch只能管理自己上游发生的异常,对于它下游的异常则无能为力,用代码来展示一下他们的区别:

  • 上游发生异常,在异常后捕获
fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            println("filter")
            it > 2
        }.map {
            println("map")
            it * 2
        }.map {
            it / 0
        }.catch {
            println("catch:$it")
        }.collect {
            println("it:$it")
        }
}

//输出结果:
//filter
//filter
//filter
//filter
//map
//catch:java.lang.ArithmeticException: / by zero
  • 上游捕获异常,下游发生异常
fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            println("filter")
            it > 2
        }.map {
            println("map")
            it * 2
        }.catch {
            println("catch:$it")
        }.map {
            it / 0
        }.collect {
            println("it:$it")
        }
}

//输出结果:
//filter
//filter
//filter
//filter
//map
 java.lang.ArithmeticException: / by zero

从两段代码可以非常清楚的总结出:上游发生异常并在异常后捕获是不会造成程序终止的,而在上游捕获异常下游发生异常时则会造成程序终止。

那么下游的异常就无法捕获了吗?并不是,对于下游的异常可以考虑采用最传统的做法try catch

fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            println("filter")
            it > 2
        }.map {
            println("map")
            it * 2
        }.catch {
            println("catch:$it")
        }.map {
            try {
                it / 0
            } catch (e: Exception) {
                println("catch:${e.message}")
            }
        }.collect {
            println("it:$it")
        }
}

//输出结果:
//filter
//filter
//filter
//filter
//map
//catch:/ by zero
//it:kotlin.Unit
//filter
//map
//catch:/ by zero
//it:kotlin.Unit

catch执行两次主要是因为前面的操作符返回的结果。

所以一句话总结就是:Flow中的catch操作符的作用与它所在的位置是强相关的,catch无法捕获的可以采用try catch捕获。

4.Flow特有的操作符——切换Context:flowOn、launchIn

Flow因为它具有上游、中间操作符、下游的特性,使得它可以处理复杂且异步执行的任务,那么异步执行的任务中大多又涉及到线程切换,Flow也恰好提供了线程切换的API。

  • flowOn
fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            logX("filter:$it")
            it > 2
        }
        .flowOn(Dispatchers.IO)			//变化在这里
        .map {
            logX("map:$it")
            it * 2
        }
        .collect {
            logX("it:$it")
        }
}

//输出结果:
//================================
//filter:0 
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//filter:1 
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//filter:2 
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//filter:3 
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//filter:4 
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//map:3 
//Thread:main
//================================
//================================
//it:6 
//Thread:main
//================================
//================================
//map:4 
//Thread:main
//================================
//================================
//it:8 
//Thread:main
//================================

flowOn线程的切换范围与catch一样仅针对上游,那么要制定collect中的Context该怎么办?可以使用withContext,但是如果除了collect之外还想让其他操作符也运行在collect所在的线程中就会遇到问题,虽然依旧可以使用withContext但是这样的写法就会很丑陋,就像下面这样失去了原本简洁的链式调用。那么解决这个问题的另一种方案launchIn就派上用场了。

fun flowTest() = runBlocking {
    withContext(myDispatcher) {
        flowOf(0, 1, 2, 3, 4)
            .filter {
                logX("filter:$it")
                it > 2
            }
            .flowOn(Dispatchers.IO)
            .map {
                logX("map:$it")
                it * 2
            }
            .collect {
                logX("it:$it")
            }
    }
}
  • launchIn
fun flowTest() = runBlocking {
    flowOf(0, 1, 2, 3, 4)
        .filter {
            logX("filter:$it")
            it > 2
        }
        .flowOn(Dispatchers.IO)
        .map {
            logX("map:$it")
            it * 2
        }
        .onEach {						//onEach实现类似 collect{} 的功能
            logX("onEach:$it")
        }
        .launchIn(CoroutineScope(myFlowDispatcher))
}

//输出结果:
//map{}、onEach{}、flow{}运行在myFlowDispatcher
//filter{}运行在DefaultDispatcher
//launchIn源码
/**
 * 在[作用域][启动][启动]给定流的[集合][收集]的终端流操作符。
 * 它是‘scope’的缩写。启动{flow.collect()} '。
 * 该操作符通常与[onEach][onCompletion][catch]操作符一起使用,
 * 处理所有发出的值,处理可能在上游流或处理过程中发生的异常,
 */
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
    collect() 
}

使用了launchIn操作符的flow无法再调用collect,从launchIn源码中可知,launchIn调用了collect()

3.Flow下游——终止操作符

Flow中最常见的操作符就是collect,除此之外还有first()single()fold{}reduce{}。这几个操作符

/**
 * 终端操作符,它返回流发出的第一个元素,然后取消流的集合
 */
public suspend fun <T> Flow<T>.first(): T {}

/**
 * 等待且仅等待一个值发出的终端操作符。对空流抛出NoSuchElementException,
 * 对包含多个元素的流抛出IllegalStateException。
 */
public suspend fun <T> Flow<T>.single(): T {}

/**
 * 从初始值开始累加值,并应用操作累加器、累加值和每个元素
 */
public suspend inline fun <T, R> Flow<T>.fold(
    initial: R,
    crossinline operation: suspend (acc: R, value: T) -> R
): R {}

/**
 * 从第一个元素开始累加值,并对当前累加器值和每个元素应用操作。如果流为空,则抛出NoSuchElementException。
 */
public suspend fun <S, T : S> Flow<T>.reduce(operation: suspend (accumulator: S, value: T) -> S): S {}

另外,当Flow调用toList转换成集合后toList后面的API都不再属于Flow因此这也就说明toList也算是一种终止操作符。

4.Flow的特点

  • Flow是冷的,只有接收方存在才会工作;
  • Flow是懒得,一次只发送一条数据。