Flow 数据流学习-冷流和热流

文章参考的Kotlin 学习笔记(五)—— Flow 数据流学习实践指北(一) - 掘金

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 里面就接收不到异常信息了,如图所示。

  • 27
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值