文章参考的Kotlin 学习笔记(五)—— Flow 数据流学习实践指北(一) - 掘金
冷流(Cold Flow):在数据被使用方订阅后,即调用 collect 方法之后,提供方才开始执行发送数据流的代码,通常是调用 emit 方法。即不消费,不生产,多次消费才会多次生产。使用方和提供方是一对一的关系。
热流(Hot Flow):无论有无使用方,提供方都可以执行发送数据流的操作,提供方和使用方是一对多的关系。热流就是不管有无消费,都可生产。
SharedFlow 就是热流的一种,任何流也可以通过 stateIn 和 shareIn 操作转化为热流,或者通过 produceIn 操作将流转化为一个热通道也能达到目的。本篇只介绍冷流相关知识,热流会在后面小节讲解~
Flow 的构造方法有如下几种:
1、 flowOf() 方法。用于快速创建流,类似于 listOf() 方法
//code 3
val testFlow = flowOf(65,66,67)
lifecycleScope.launch {
testFlow.collect {
println("输出:$it")
}
}
//打印结果:
//输出:65
//输出:66
//输出:67
2、asFlow() 方法。是集合的扩展方法,可将其他数据转换成 Flow,例如 Array 的扩展方法
//code 4
public fun <T> Array<T>.asFlow(): Flow<T> = flow {
forEach { value ->
emit(value)
}
}
不仅 Array 扩展了此方法,各种其他数据类型的数组都扩展了此方法。所以集合可以很方便地构造一个 Flow
3、flow {···} 方法。这个方法可以在其内部顺序调用 emit 方法或 emitAll 方法从而构造一个顺序执行的 Flow。emit 是发射单个值;emitAll 是发射一个流,这两个方法分别类似于 list.add(item)、list.addAll(list2) 方法。flow {···} //code 6
需要额外注意的是,flow 后面的 lambda 表达式是一个挂起函数,里面不能使用不同的 CoroutineContext 来调用 emit 方法去发射值。因此,在 flow{...} 中不要通过创建新协程或使用 withContext 代码块在另外的 CoroutineContext 中调用 emit 方法,否则会报错。如果确实有这种需求,可以使用 channelFlow 操作符。
val testFlow = flow {
emit(23)
// withContext(Dispatchers.Main) { // error
// emit(24)
// }
delay(3000)
emitAll(flowOf(25,26))
}
4、channelFlow {···} 方法。这个方法就可以在内部使用不同的 CoroutineContext 来调用 send 方法去发射值,而且这种构造方法保证了线程安全也保证了上下文的一致性
//code 8
val testFlow1 = channelFlow {
send(20)
withContext(Dispatchers.IO) { //可切换线程
send(22)
}
}
lifecycleScope.launch {
testFlow1.collect {
println("输出 = $it")
}
}
3. Flow 常用的操作符
Flow 的使用依赖于众多的操作符,这些操作符可以大致地分为 中间操作符 与 末端操作符 两大类。中间操作符是流上的中间操作,可以针对流上的数据做一些修改,是链式调用。中间操作符与末端操作符的区别是:中间操作符是用来执行一些操作,不会立即执行,返回值还是个 Flow;末端操作符就会触发流的执行,返回值不是 Flow
一个完整的 Flow 是由 Flow 构建器、Flow 中间操作符、Flow 末端操作符 组成,如下示意图所
flow{...} .map{...}.filter{...}.take() .toList()
构建器 中间操作符 末端操作符
3.1 collect 末端操作符
最常见的当然是 collect 操作符。它是个挂起函数,需要在协程作用域中调用;并且它是一个末端操作符,末端操作符就是实际启动 Flow 执行的操作符,这一点跟 RxJava 中的 Observable 对象的执行很像。
熟悉 RxJava 的同学知道,在 RxJava 中,Observable 对象的执行开始时机是在被一个订阅者(subscriber) 订阅(subscribe) 的时候,即在 subscribe 方法调用之前,Observable 对象的主体是不会执行的。
Flow 也是相同的工作原理,Flow 在调用 collect 操作符收集流之前,Flow 构建器和中间操作符都不会执行。举个栗子:
//code 9
val testFlow2 = flow {
println("++++ 开始")
emit(40)
println("++++ 发出了40")
emit(50)
println("++++ 发出了50")
}
lifecycleScope.launch {
testFlow2.collect{
println("++++ 收集 = $it")
}
}
// 输出结果:
//com.example.myapplication I/System.out: ++++ 开始
//com.example.myapplication I/System.out: ++++ 收集 = 40
//com.example.myapplication I/System.out: ++++ 发出了40
//com.example.myapplication I/System.out: ++++ 收集 = 50
//com.example.myapplication I/System.out: ++++ 发出了50
从输出结果可以看出,每次到 collect 方法调用时,才会去执行 emit 方法,而在此之前,emit 方法是不会被调用的。这种 Flow 就是冷流。
3.2 reduce末端操作符
reduce 也是一个末端操作符,它的作用就是将 Flow 中的数据两两组合接连进行处理,跟 Kotlin 集合中的 reduce 操作符作用相同。举个栗子:
//code 10
private fun reduceOperator() {
val testFlow = listOf("w","i","f","i").asFlow()
CoroutineScope(Dispatchers.Default).launch {
val result = testFlow.reduce { accumulator, value ->
println("+++accumulator = $accumulator value = $value")
"$accumulator$value"
}
println("+++final result = $result")
}
}
//输出结果:
//com.example.myapplication I/System.out: +++accumulator = w value = i
//com.example.myapplication I/System.out: +++accumulator = wi value = f
//com.example.myapplication I/System.out: +++accumulator = wif value = i
//com.example.myapplication I/System.out: +++final result = wifi
看结果就知道,reduce 操作符的处理逻辑了,两个值处理后得到的新值作为下一轮中的输入值之一,这就是两两接连进行处理的意思。
3.3 zip 中间操作符
zip 顾名思义,就是可以将两个 Flow 汇合成一个 Flow,举个栗子就知道了
//code 11
lateinit var testFlow1: Flow<String>
lateinit var testFlow2: Flow<String>
private fun setupTwoFlow() {
testFlow1 = flowOf("Red", "Blue", "Green")
testFlow2 = flowOf("fish", "sky", "tree", "ball")
CoroutineScope(Dispatchers.IO).launch {
testFlow1.zip(testFlow2) { firstWord, secondWord ->
"$firstWord $secondWord"
}.collect {
println("+++ $it +++")
}
}
}
// 输出结果:
//com.example.myapplication I/System.out: +++ Red fish +++
//com.example.myapplication I/System.out: +++ Blue sky +++
//com.example.myapplication I/System.out: +++ Green tree +++
//zip 方法声明:
public fun <T1, T2, R> Flow<T1>.zip(other: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R> = zipImpl(this, other, transform)
从 zip 方法的声明中可知,zip 方法的第二个参数就是针对两个 Flow 进行各种处理的挂起函数,也可如例子中写成尾调函数的样子,返回值是处理之后的 Flow。而且当两个 Flow 长度不一样时,最后的结果会默认剔除掉先前较长的 Flow 中的元素。所以 testFlow2 中的 “ball” 就被自动剔除掉了。
4. Flow 异常处理
正如 RxJava 框架中的 subscribe 方法可以通过传入 Observer 对象在其 onNext、onComplete、onError 返回之前处理的结果,Flow 也有诸如 catch、onCompletion 等操作符去处理执行的结果。例如下面的代码:
//code 12
private fun handleExceptionDemo() {
val testFlow = (1..5).asFlow()
CoroutineScope(Dispatchers.Default).launch {
testFlow.map {
check(it != 3) {
//it == 3 时,会走到这里
println("+++ catch value = $it")
}
println("+++ not catch value = $it")
it * it
}.onCompletion {
println("+++ onCompletion value = $it")
}.catch { exception ->
println("+++ catch exception = $exception")
}.collect{
println("+++ collect value = $it")
}
}
}
//输出结果:
//com.example.myapplication I/System.out: +++ not catch value = 1
//com.example.myapplication I/System.out: +++ collect value = 1
//com.example.myapplication I/System.out: +++ not catch value = 2
//com.example.myapplication I/System.out: +++ collect value = 4
//com.example.myapplication I/System.out: +++ catch value = 3
//com.example.myapplication I/System.out: +++ onCompletion value = java.lang.IllegalStateException: kotlin.Unit
//com.example.myapplication I/System.out: +++ catch exception = java.lang.IllegalStateException: kotlin.Unit
顺着代码咱先来看看一些常用的 Flow 中间操作符。
1)map :用来将 Flow 中的数据一个个拿出来做各自的处理,然后交给下一个操作符;本例中就是将 Flow 中的数据进行平方处理;
2)check() :类似于一个检查站,满足括号内条件的数据可以通过,不满足则交给它的尾调函数处理,并且抛出异常;
3)onCompletion :Flow 最后的兜底器。无论 Flow 最后是执行完成、被取消、抛出异常,都会走到 onCompletion 操作符中,类似于在 Flow 的 collect 函数外加了个 try,finally。官方给了个小栗子,还是很清楚的:
//code 13
try {
myFlow.collect { value ->
println(value)
}
} finally {
println("Done")
}
//上述代码可以替换为下面的代码:
myFlow
.onEach { println(it) }
.onCompletion { println("Done") }
.collect()
所以,在 code 12 中的 onCompletion 操作符可以接住从 check 那儿抛出的异常;
4)catch :不用多说,专门用于捕捉异常的,避免程序崩溃。这里如果把 catch 去掉,程序就会崩溃。如果把 catch 和 onCompletion 操作符位置调换,则 onCompletion 里面就接收不到异常信息了,如图所示。