Kotlin系列之认识一下Flow

Posted datian1234

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin系列之认识一下Flow相关的知识,希望对你有一定的参考价值。

前言

Flow是谷歌官方提供的一套基于kotlin协程响应式编程模型,与我们熟知的RxJava使用起来类似,但相比较起来,Flow使用起来更加简单,而且Flow是作用在协程内,可以跟协程的生命周期绑定在一起,线程切换起来也比RxJava灵活,所以,我们学习Flow也跟学习RxJava一样,先从操作符开始学.

操作符

创建单个Flow

runBlocking 
    flow 
        emit(System.currentTimeMillis())
        delay(1000)
        emit(System.currentTimeMillis())
        delay(1000)
        emit(System.currentTimeMillis())
    .collect 
        println(it)
    

collect是一个终端操作符,用来上流传送过来的数据,这里创建了一个Flow,每间隔一秒发送一个时间戳,然后在终端将时间戳打印出来,我们得到结果

 I  1672563578934
 I  1672563579935
 I  1672563580938

创建多个Flow

runBlocking  
    flowOf(
        "小明阳了",
        "小强阳了",
        "小红阳了"
    ).collect
        println(it)
    

我们使用flowOf来发送一组Flow,区别于创建单个flow,这里不需要调用emit这样的发送函数,只要调用了终端操作符collect,flowOf里面的数据就都会发送出来,但也不能在数据之间做其余操作,比如调用delay函数,我们运行一下得到结果如下

 I  小明阳了
 I  小强阳了
 I  小红阳了

集合转化为Flow

runBlocking  
    listOf("abc","123","666").asFlow()
        .collect
            println(it)
        

我们使用asFlow操作符,来将一个集合转化为一组Flow并发送,上述运行结果为

 I  abc
 I  123
 I  666

在回调方法中发送Flow

平时开发中我们经常会遇到回调函数,比如点击个按钮在点击事件中做些操作,或者监听一个输入框,监听用户输入的内容并把内容输出,在这些案例中,我们发现如果继续用上文学到的Flow创建方式,然后在回调方法中调用emit发送函数,会出现一段提示

意思是挂起函数必须要在一个协程作用域里面才能调用,那遇到这个情况我们该怎么做呢?我们使用另一种创建方式,创建一个callbackFlow,我们将上述例子改一下变成如下样子

GlobalScope.launch(Dispatchers.Main) 
    callbackFlow 
        bindingView.callbackEdit.addTextChangedListener(object : TextWatcher 
            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) 

            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) 

            override fun afterTextChanged(p0: Editable?) 
                trySend(p0)
            

        )
        awaitClose  
    .collect
        println(it)
    

我们看到将创建Flow的方式从flow变成callbackFlow,内部发送函数从emit改成trySend,然后注意的是,使用callbackFlow必须要在最后调用awaitClose关闭资源,不然程序会闪退,我们运行一遍上述程序,出来这样一个界面

我们在编辑框中依次输入a,b,c

我们看下控制台打印出来的信息

 I  a
 I  ab
 I  abc

可以看到跟每次输入完以后的内容一致

终端操作符collectLatest

这个操作符跟之前遇到的collect一样,也是终端操作符,区别在于它接受最新一条数据,如果新的数据来的时候,上一条数据未处理完,则取消上一条数据,是处理背压现象的一种操作符,我们用代码举个例子

runBlocking 
    var start = System.currentTimeMillis()
    flow 
        emit(1)
        delay(1000)
        emit(2)
        delay(500)
        emit(3)
        delay(600)
        emit(4)
    .collectLatest 
        delay(1000)
        var end = System.currentTimeMillis()
        println("经过$end - start,收到$it")
    

这里上游先发送1,然后等待1秒再发送2,再等待0.5秒,然后发送个3,等待0.6秒,再发送4,下游收到数据以后,会先等待1秒,然后再处理数据,我们通过collectLatest特性不难推算出,发送1以后,下游等待1秒去处理数据1,这个时候,上游也过了1秒,然后发送数据2,在下游等待1秒的过程中上游过了0.5秒又发送了数据3,所以数据2被取消,发送数据3以后,在下游经过1秒的等待时间,上游过了0.6秒又发送了数据4,所以数据3同样也取消,数据4可以被处理,所以只有1跟4能够被下游接受到并处理掉,我们运行一遍看下结果是不是也一样

 I  经过1002,收到1
 I  经过3107,收到4

我们看到的确是收到1跟4,而且收到4的时候,总共也就过去了3秒,说明数据2跟3在下游的处理操作是被取消掉的,并没有把他们delay的时间算上去

map操作符

这个操作符大家应该都很熟悉,无论是在RxJava还是kotlin自带的操作符中都有遇到过,作用就是改变上游数据,我们举个例子看看

runBlocking 
    flowOf(
        "小红喝可乐","小强喝可乐","小明喝可乐"
    ).map 
        var exp = it
        if(it.startsWith("小强"))
            exp = "小强喝咖啡"
        
        exp
    .collect
        println(it)
    

这里上游发送三条数据,分别是小红喝可乐,小强喝可乐,小明喝可乐,然后经过map操作符的时候,将小强的数据改成小强喝咖啡,我们看下最终打印结果

 I  小红喝可乐
 I  小强喝咖啡
 I  小明喝可乐

可以看到小强那段数据改变了,map操作符在实际开发过程中,有一个很常见的场景可以使用到,那就是服务端下发的数据模型,与我们前端需要展示的数据模型不一致,那就需要map在中间做一层转换,变成我们需要的数据模型。

filter操作符

filter操作符是将不满足条件的数据过滤出去,只保留符合条件的数据,我们将map的例子改一下

runBlocking 
    flowOf(
        "小红喝可乐","小强喝可乐","小明喝可乐"
    ).filter 
        it.startsWith("小强")
    .collect
        println(it)
    

上述例子改成,只输出小强的数据,其他数据都过滤掉,我们运行下看看结果

 I  小强喝可乐

结果也跟我们预期的一致,filter操作符在实际开发中可用于将一些服务端下发的数据中,不需要展示给用户的数据过滤掉,比如商品过期,下架等情况

drop操作符

这个操作符入参是一个整数,表示丢弃前n个数据,我们用一段代码演示下

runBlocking 
    flow 
        emit(1)
        emit(2)
        emit(3)
        emit(4)
        emit(5)
    .drop(2)
        .collect 
            println(it)
        

这里发送1至5五个数据,drop入参2,表示丢弃前2个数据,那么很明显最终输出的是3,4,5,我们运行下看看结果

 I  3
 I  4
 I  5

结果跟我们预期的一致

dropWhile操作符

这个操作符的作用是,找到第一个不符合条件的值,返回其后面所有的值,若第一个不满足,返回全部,我们同样将上述例子改一下

runBlocking 
    flow 
        emit(1)
        emit(2)
        emit(3)
        emit(4)
        emit(5)
    .dropWhile 
        it < 4
    .collect 
        println(it)
    

这里表示,找到第一个满足条件<4的数据,返回其以及之后的所有数据,我们看下运行结果

 I  4
 I  5

显然只输出4和5,dropWhile在实际开发当中可用在如下场景,比如返回当月所有订单数据,然后要求每一天看到的数据都是从当天开始往后的数据,那么dropWhile就要将当天之前的数据都排除过滤掉

take操作符

take跟drop一样,也接受一个整型数据,区别是take是只获取前n个数据,我们用一段代码演示一下

runBlocking 
    flow 
        emit(1)
        emit(2)
        emit(3)
        emit(4)
        emit(5)
    .take(3).collect 
        println(it)
    

同样也是发送五个数据,我们在下游用take操作符截取三个数据,那么打印出来的应该是1,2,3,运行下看看结果如何

 I  1
 I  2
 I  3

果然结果就是1,2,3,take操作符在实际开发场景中用途也很广泛,常见的是一个数据列表,在首页只展示这个列表的前几个数据,点击更多跳到下一页才把剩余数据展示出来,这里前几个数据就可以用take去截取展示

takeWhile操作符

takewhile跟dropWhile用法一样,也是在方法体中加入判断条件,区别在于,takeWhile是找第一个不符合条件的值,取前面的值,若第一个就不符合,返回空,我们用一段代码演示一下

runBlocking 
    flow 
        emit(1)
        emit(2)
        emit(3)
        emit(4)
        emit(5)
    .takeWhile  it < 4 .collect 
        println(it)
    

同样也是发送五个数据,我们在takewhile中加入条件是小于4的值,那么我们就看第一个不符合这个条件的值是哪个,显然是4,那么返回的自然就是1,2,3这三个数据,我们运行下代码看看结果如何

 I  1
 I  2
 I  3

结果也不出所料,takewhile在实际开发过程中的使用场景跟dropWhile的场景比较类似,同样也是获取一个月的订单数据,然后有一个页面需要展示今天之前的历史订单,那么takeWhile就可以把今天之前的历史订单数据给过滤出来

回调操作符onStart,onEach,onCompletion

onStart操作符是在一段数据操作之前调用的方法,onEach是在数据操作过程中调用的方法,onCompletion是一段数据操作结束以后调用的方法,乍一看跟我们平时的网络请求的回调很相似,我们用一段代码演示一下这三个操作符的特性

runBlocking 
    flowOf(
        "小明学语文",
        "小强学数学",
        "小红学物理"
    ).onStart 
        println("开始。。。")
    .onEach 
        println("改变前。。$it")
    .map 
        var exp = it
        if (exp.startsWith("小红")) 
            exp = "在学校,$exp"
        
        exp
    .onCompletion 
        println("结束。。。")
    .collect 
        println("改变后。。$it")
    

我们这里发送三个数据,“小明学语文”,“小强学数学”,“小红学物理”,然后在中间对小红那条数据,我们在前面加上"在学校"三个字,不是小红的就不加,这个过程中会在map前加上onEach方法把转变前的数据打印出来,然后在collect中把转变后的数据打印出来,这个过程的开始与结束会分别调用onStart跟onCompletion方法来加上标识符,我们运行下这段代码看下结果如何

 I  开始。。。
 I  改变前。。小明学语文
 I  改变后。。小明学语文
 I  改变前。。小强学数学
 I  改变后。。小强学数学
 I  改变前。。小红学物理
 I  改变后。。在学校,小红学物理
 I  结束。。。

我们看到,很清晰的将这个数据转变过程给打印出来了,回调操作符想必大家也都清楚了,最常用的场景就是在我们的网络框架中,在请求的开始,请求中,以及请求结束分别做对应的操作,比如加载框的初始化,弹出,以及加载框的关闭

debounce操作符

这个操作符是用来处理数据发送过于频繁而简化上游数据,debounce会传入一个时间,如果上游数据之间发送的时间间隔小于debounce设置的时间,那么会取消最先发送出来的数据,我们用代码演示一下

runBlocking 
    flow 
        emit(1)
        delay(300)
        emit(2)
        delay(800)
        emit(3)
        emit(4)
        delay(300)
        emit(5)
    .debounce(600).collect 
        println(it)
    

同样发送五个数据,我们在1跟2之间间隙300毫秒,在2跟3之间间隙800毫秒,4跟5之间间隙300毫秒,debounce设置600毫秒,意思就是如果数据之间发送间隔小于600毫秒的话,那么就取消发送第一条数据,我们运行一下看看结果

 I  2
 I  5

我们看到由于1跟2之间是300毫秒间隔,小于600毫秒,所以取消发送数据1,数据2跟3之间间隔是800毫秒,大于600毫秒,所以数据2成功发送,3跟4之间没有时间间隔,所以3也取消发送,4跟5之间是300毫秒,也小于600毫秒,所以4也取消发送,剩下数据5因为后面没有数据了,所以数据5也是成功发送,最终我们看到的结果就是收到了2跟5

这个操作符在我们搜索框里面输入东西查询结果时候很常用,因为我们打字的速度都比较快,如果每输入一个字就去查询结果,那么等结果出来以后,输入框里面已经不是之前输入的内容了,所以正确的做法也是当输入过程中出现间隙的时候,再将输入框中的内容拿去请求结果,这样不管是在流量还是性能上都是最佳的做法

采样操作符sample

sample操作符会经过一段时间去上游去取数据,专门针对上游数据量比较庞大,然后并不是每一条数据都比较重要的情况,我们用一段代码演示一遍

runBlocking 
    flow 
        var num = 1
        while (num < 20)
            emit(num)
            delay(500)
            num++
        
    .sample(1000).collect
        println(it)
    

这里每间隔500毫秒会发送一条数据,从1开始,逐个累加到20,然后我们采样操作符设定了每过1秒取一次数据,我们看看最终打印出来的结果如何

 I  2
 I  4
 I  6
 I  8
 I  10
 I  12
 I  14
 I  16
 I  18

可以看到只输出了刚好间隔为1秒的数据,其余数据就直接被过滤掉了,sample操作符使用的场景是数据量比较庞大,但并不需要展示所有的情况,比如视频弹幕,每分钟可能弹幕有很多条,但是显示在屏幕上的弹幕就只有几条,毕竟如果都显示在屏幕上我们就没必要去看视频了

规约操作符reduce

reduce操作符是具有累加的功能,它跟我们之前遇到的collect不一样,它返回的是一个累加的结果,我们用一段代码演示一下这个操作符的特性

runBlocking 
    var result = listOf("he", "llo", ",w", "or", "ld").asFlow()
        .reducea,b -> a+b
    println("输出结果:$result")

代码中我们看到,有一组字符串的数组,它们转换成flow之后进行reduce操作,意思就是对字符串逐个累加,reduce里面的a跟b第一次表示累加的初始两个值,后面则一个表示之前累加的结果,另一个表示接下去需要累加的值,我们运行一遍上述代码得到结果如下

 I  输出结果:hello,world

规约操作符fold

这个操作符与之前讲的reduce操作符用法基本一致,区别在于fold接受一个初始值,我们将上述代码更改一下,用fold实现一遍

runBlocking 
    var result = listOf("he", "llo", ",w", "or", "ld").asFlow()
        .fold("Testing...")a,b -> a+b
    println("输出结果:$result")

我们在之前输出的结果前面加上了Testing…字样,运行一遍代码得到结果如下

I  输出结果:Testing...hello,world

flatMapConcat

之前我们讲的都是只操作一个Flow,接下去我们要讲操作两个或多个Flow,首先我们先看下flatMapConcat操作符,这个操作符我们解释一下就是多个Flow按照先后顺序串行执行,我们用一段代码演示一下

runBlocking 
    flowOf(
        "小明",
        "小强",
        "小红"
    ).flatMapConcat 
        val start = System.currentTimeMillis()
        flow 
            when (it) 
                "小明" -> delay(3000)
                "小强" -> delay(2000)
                "小红" -> delay(1000)
            
            var dis = System.currentTimeMillis() - start
            emit("用时$dis,$it去上学")
        
    .collect 
        println(it)
    

这里第一个流依次发送小明,小强,小红,第二个流在小明到来时候停顿3秒,小强停顿两秒,小红停顿1秒,最终打印出来看下它们的执行顺讯以及用时多少

 I  用时3001,小明去上学
 I  用时2000,小强去上学
 I  用时1001,小红去上学

可以看到还是按照第一个流的发送顺序依次执行,就算小红花的时间最少,也要等小明跟小强执行完毕以后在去执行,这个就是flatMapConcat的特性,适合用在前后任务有强烈依赖关系的情况下,比如获取一个用户信息,必须拿到这个用户的id,这个id必须是登录之后才能拿到,所以必须是先调用完登录接口之后拿到id再去调用用户信息接口,顺序不能错

flatMapMerge

这个操作符与flatMapConcat类似,也是多个flow一起执行,区别在于flatMapMerge没有顺序要求,哪个先执行完哪个就先输出,我们将上述例子改一下

runBlocking 
    flowOf(
        "小明",
        "小强",
        "小红"
    ).flatMapMerge 
        val start = System.currentTimeMillis()
        flow 
            when (it) 
                "小明" -> delay(3000)
                "小强" -> delay(2000)
                "小红" -> delay(1000)
            
            var dis = System.currentTimeMillis() - start
            emit("用时$dis,$it去上学")
        
    .collect 
        println(it)
    

改动不大,仅仅是将flatMapConcat的位置改成flatMapMerge,我们看下最终结果有什么区别

 I  用时1001,小红去上学
 I  用时2001,小强去上学
 I  用时3001,小明去上学

可以看到每个数据的最终用时没有什么区别,但是在执行顺序上,小红由于用时最少先执行完,所以第一个输出的就是小红,其次是小强,小明由于用时最多,所以是最后一个输出

flatMapLatest

这个操作符就跟collectLatest一样,就是获取最新的数据,这边就是从第一个流传输到第二个流的时候,如果第一个流里面又有新的数据来了,而之前的流的数据并没有处理完,那么就取消之前流的数据的操作,执行新的流的数据,我们用一段代码演示一下

runBlocking 
    flow
        emit("小强")
        delay(1000)
        emit("小明")
        delay(400)
        emit("小红")
        delay(600)
        emit("小李")
    .flatMapLatest 
        flow 
            delay(1000)
            emit("$it从学校回来")
        
    .collect
        println(it)
    

这边第一个流每过一段时间就会发送一条数据,在第二个流里面,先停顿1秒,然后拼接一个字符串在输出,最终打印出结果,我们看下代码的最终运行结果

 I  小强从学校回来
 I  小李从学校回来

根据flatMapLatest的特性,小强发送出去以后在第二个流里面停顿1秒钟在操作,此时第一个流也过去了1秒才有新的数据发送出来,所以小强是可以输出的,小明在发送出去的时候,只过去了0.4秒又发送了小红,所以小明在第二个流里面并没有得到处理就被取消了,而小红在发送出去之后,仅仅过了0.6秒又发送了一个小李,所以小红也没有得到处理就被取消掉了,最后小李后面因为没有其他数据了,小李最终也被输出

zip

之前讲的都是从一个流传到另一个流,两个流之间是串行运行的,而zip操作符则有点区别,它是将两个流并行运行,一个流里面的数据执行完毕,会等待另一个流里面的数据,等都执行完毕以后才会输出,我们举个例子来看下

runBlocking 
    flow 
        emit("a1发送时间$System.currentTimeMillis()\\n")
        delay(1000)
        emit("a2发送时间$System.currentTimeMillis()\\n")
        delay(1000)
        emit("a3发送时间$System.currentTimeMillis()\\n")
    .zip(flow 
        emit("b1发送时间$System.currentTimeMillis()")
        delay(3000)
        emit("b2发送时间$System.currentTimeMillis()")
        delay(1000)
        emit("b3发送时间$System.currentTimeMillis()")
        delay(1000)
        emit("b4发送时间$System.currentTimeMillis()")
    )  a, b ->
        a + b
    .collect 
        println("$it\\n-------------------")
    

直接运行看下结果

 I  a1发送时间1672713340429
 I  b1发送时间1672713340429
 I  -------------------
 I  a2发送时间1672713341431
 I  b2发送时间1672713343430
 I  -------------------
 I  a3发送时间1672713344432
 I  b3发送时间1672713344431
 I  -------------------

我们发现a1跟b1是先被同时输出,接着a2过了1秒以后也被发送出去,但没有被输出,a2在等待b2执行完毕,b2在等待3秒以后也被发送出去,这个时候a2与b2才同时被输出,紧接着过了1秒,a3与b3也被输出,此时由于第一个流里面任务已经都被执行完了,所以b4是不会被输出。zip操作符我们可以用在,当一个页面有多个接口时候,我们想要获取多个接口执行完毕的回调,来执行一些比如重置刷新或者关闭加载框的动作,我们就可以使用这个操作符来实现

buffer

之前我们讲了一个collectLatest,它是用来处理背压现象的一种操作符,接下去我们来讲另外两个处理背压现象的操作符,其中一个就是buffer,这个操作符从字面上就能看出,它是启缓冲作用,如何缓冲呢?它是将上游流速过快,导致下游没来得及处理的一些数据,放在一个缓存堆里面,下游直接从缓存堆里面拿数据,不受上游的流速影响,我们用代码来加深一下对这个操作符的理解

runBlocking 
    var start = System.currentTimeMillis()
    flow 
        var value = 1
        while (value < 10) 
            emit(value)
            delay(500)
            value++
        
    .collect 
        delay(1000)
        var end = System.currentTimeMillis()
        println("收到上游数据--$it--用时$end - start")
    

这里是一个很简单的上游发送数据,下游接受数据的例子,我们看到上游每发送一个数据都要停顿0.5秒,而下游在接收到数据后也停顿1秒才去处理数据,其实从设计上来讲,我们不关心上游如何发送数据,我们只想要每一秒钟将数据打印出来,然而我们看下最终结果是不是我们预期的那样呢

 I  收到上游数据--1--用时1002
 I  收到上游数据--2--用时2504
 I  收到上游数据--3--用时4005
 I  收到上游数据--4--用时5506
 I  收到上游数据--5--用时7008
 I  收到上游数据--6--用时8515
 I  收到上游数据--7--用时10026
 I  收到上游数据--8--用时11530
 I  收到上游数据--9--用时13034

我们看到了,每一个数据用时都要超过1秒,明显是受到上游流速影响了,那我们该怎么修改呢,这个时候buffer的作用就体现出来了,我们把buffer加入到上面的例子中去试试

runBlocking 
    var start = System.currentTimeMillis()
    flow 
        var value = 1
        while (value < 10) 
            emit(value)
            delay(500)
            value++
        
    .buffer().collect 
        delay(1000)
        var end = System.currentTimeMillis()
        println("收到上游数据--$it--用时$end - start")
    

改动不大,就是在上下游之间加入了buffer操作符,作用就是将上游的数据先缓存起来,等下游数据处理完以后,直接从缓存中拿数据,我们来看下现在运行的结果如何

 I  收到上游数据--1--用时1002
 I  收到上游数据--2--用时2003
 I  收到上游数据--3--用时3003
 I  收到上游数据--4--用时4005
 I  收到上游数据--5--用时5005
 I  收到上游数据--6--用时6006
 I  收到上游数据--7--用时7007
 I  收到上游数据--8--用时8010
 I  收到上游数据--9--用时9014

可以看到,现在的结果才是我们想要的,每一条数据都是间隔1秒才被打印出来

conflate

我们讲了两个处理背压现象的操作符,现在来讲最后一个,在刚刚的例子中,大家有没有注意到,我们每过一秒去缓存里面拿的数据,其实已经不是最新数据了,在一些券商的app中,绘制k线图的时候,行情数据就算下发的再快,我们界面上定时刷新绘制的数据永远都是最新的,用户并不关心你几秒前的行情数据到底多少,他们只关心最新的数据,所以我们在对于处理上游流速过快的问题上,也只需要关心最新数据即可,这个时候我们就要用到另一个操作符conflate,同样我们将之前的例子改一下

runBlocking 
    val start = System.currentTimeMillis()
    flow 
        var value = 0
        while (value < 20) 
            emit(value)
            delay(500)
            value++
        
    .conflate().collect 
        delay(1000)
        var end = System.currentTimeMillis()
        println("收到上流数据--$it--用时$end - start")
    

这里我们将上游发送的数据扩增10个,在下游获取数据之前,我们加上了conflate操作符,意思是每秒钟只取最新的数据,我们运行一遍看看结果

 I  收到上流数据--0--用时1002
 I  收到上流数据--1--用时2004
 I  收到上流数据--3--用时3004
 I  收到上流数据--5--用时4005
 I  收到上流数据--7--用时5006
 I  收到上流数据--9--用时6007
 I  收到上流数据--11--用时7010
 I  收到上流数据--13--用时8015
 I  收到上流数据--15--用时9022
 I  收到上流数据--17--用时10025
 I  收到上流数据--19--用时11031

我们看到每秒钟输出的数据不是连贯的,因为当上一条数据处理完以后,上游又有新的数据来了,所以只会拿最新的数据而不会按照发送顺序将老的数据输出,这样处理背压的三个操作符都讲完了,大家在日常开发中可以按需使用。

冷流与热流

冷流

  • 一般创建的flow都属于冷流
  • 冷流在无订阅者的情况下,不会产生数据
  • 冷流里面生产者跟订阅者属于一对一关系,同一个订阅者多次订阅的时候,有且只有一个生产者对它发送数据

热流

  • 分为SharedFlow跟StateFlow
  • 热流是在无订阅者的情况下,也会产生数据
  • 热流中生产者跟订阅者是一对多关系,即同一个生产者,同时可以由多个订阅者同时订阅
  • SharedFlow可缓存数据,同一份数据可发送给多个订阅者
  • StateFlow每次只发送一条数据的SharedFlow,有初始值

SharedFlow

我们先来看下SharedFlow哪里体现出可缓存数据,哪里体现出一份数据可发送给多个订阅者,我们创建SharedFlow的方式有两种,一种是通过MutableSharedFlow的构造方法,另一种是通过普通Flow调用shareIn来创建,我们主要看下shareIn方法,因为shareIn内部其实就用到了MutableSharedFlow的构造方法生成SharedFlow

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow<T> 
    val config = configureSharing(replay)
    val shared = MutableSharedFlow<T>(
        replay = replay,
        extraBufferCapacity = config.extraBufferCapacity,
        onBufferOverflow = config.onBufferOverflow
    )
    @Suppress("UNCHECKED_CAST")
    val job = scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)
    return ReadonlySharedFlow(shared, job)

我们看到shareIn接收三个参数,第一个参数就是协程的作用域,没啥说的,第二个参数是控制共享流开始与结束的策略,它总共有三个选项

  • Eagerly 共享数据立马发送,并且永不终止
  • Lazily 共享数据直到出现第一个订阅者的时候才开始发送,并且永不终止
  • WhileSubscribed 第一个订阅者出现时候开始发送数据,并默认在最后一个订阅者消失时候停止发送数据,数据缓存也是默认永远存在,WhileSubscribed有两个参数,stopTimeoutMillis表示最后一个订阅者消失时候停止发送数据的延迟时间,默认为0,第二个参数为replayExpirationMillis,表示缓存数据的过期时间,时间一到就会重置缓存池 第三个参数replay为缓存数量,表示发送到缓存池里面的数据个数

我们用一段代码示例来进一步讲解下SharedFlow

var num = 0
var numFlow = flow 
    num++
    emit("第$num条数据 $System.currentTimeMillis()")
    emit("第$num + 1条数据 $System.currentTimeMillis()")
    emit("第$num + 2条数据 $System.currentTimeMillis()")
.shareIn(lifecycleScope, SharingStarted.Eagerly, 2)
bindingView.sharedflowBtn.setOnClickListener 
    runBlocking 
        launch 
            numFlow.collect 
                println("$System.currentTimeMillis() 订阅者1收到 $it")
            
        
        launch 
            numFlow.collect 
                println("$System.currentTimeMillis() 订阅者2收到 $it")
            
        
    

我们创建一个普通流,流里面发送三条数据,每一条数据都有它的下标值以及发送时间,随后调用shareIn来将这个普通流转化成SharedFlow,shareIn里面的策略参数选用Eagerly,表示立刻发送,缓存数量设置为2,表示只在缓存中写入两条数据,我们又新增一个按钮,点击时候生成两个订阅者,每个订阅者都打印出接收到数据的时间以及数据内容,我们运行一遍上述代码,看看点击按钮以后的输出内容

 I  1672719702780 订阅者1收到 第2条数据 1672719685184
 I  1672719702781 订阅者1收到 第3条数据 1672719685184
 I  1672719702781 订阅者2收到 第2条数据 1672719685184
 I  1672719702781 订阅者2收到 第3条数据 1672719685184

我们看到两个订阅者收到的数据完全一样,表示同一份数据多个订阅者共享,打印出来的结果里面,前面的时间略大于后面的时间,表示数据已经在订阅者出现之前就已经发送出来,只是保存在缓存池里面,当订阅者出现之后,才从缓存池中拿出数据,另外,订阅者只接收到第2,3条数据,第一条数据没有了,从而也证明了replay参数设置起了作用。我们再将shareIn的第二个参数改成Lazily,其他的都不变,试试看运行出来的结果有什么不一样的地方

 I  1672722732294 订阅者1收到 第1条数据 1672722732293
 I  1672722732294 订阅者1收到 第2条数据 1672722732294
 I  1672722732294 订阅者1收到 第3条数据 1672722732294
 I  1672722732294 订阅者2收到 第2条数据 1672722732294
 I  1672722732294 订阅者2收到 第3条数据 1672722732294

我们看到结果稍微有一点不一样,首先订阅者1完全接收到了所有三条数据,证明缓存只在数据发送一次以后才会保存进去,订阅者2也证明了这一点,其次发送数据和接收数据的时间完全一样,证明了Lazily是在订阅者出现之后才能发送数据,没有订阅者数据不会发送。

我们现在对SharedFlow有了一定的认识了,我们再看看StateFlow

StateFlow

StateFlow可以理解为缓存池大小为1的SharedFlow,StateFlow生成的方式也有两种,一种是MutableStateFlow的构造方法,方法参数只有一个initvalue,初始默认值,表示没有数据时候默认发送的一条数据,类似于我们页面上的空态页,另一种方式是调用stateIn方法,来将普通Flow转换成StateFlow,stateIn的入参也有三个,前两个同shareIn一样,第三个是默认值initValue,我们来用一段代码演示一遍

var numFlow = flow 
    emit("喜羊羊")
    emit("美羊羊")
.stateIn(lifecycleScope, SharingStarted.Lazily, "灰太狼")
bindingView.stateflowBtn.setOnClickListener 
    runBlocking 
        launch 
            numFlow.collect 
                println("订阅者1获取值 $it")
            
        
        launch 
            numFlow.collect 
                println("订阅者2获取值 $it")
            
        
    

同样也有一个普通流,接连发送两个数据分别是喜羊羊和美羊羊,然后调用stateIn方法转换成StateFlow,stateIn的策略参数选用Lazily,默认值为灰太狼,同样也有一个按钮,点击之后生成两个订阅者,都输出接收到的内容,我们运行一遍代码看看结果

 I  订阅者1获取值 美羊羊
 I  订阅者2获取值 美羊羊

很明显,作为缓存池大小只有1的StateFlow来说,每一次只发送一条数据,所以订阅者收到的数据是最新一条数据也就是美羊羊,那这个时候我们就有个疑惑了,我们设置的默认值灰太狼什么时候出现呢?大家是否还记得刚刚举的空态页的例子,我们都知道一般性空态页是在请求接口等待数据返回的过程中在屏幕上起到占位的作用,那我们也模拟一个等待数据的过程,很简单,在emit函数之前加上delay函数,时间我们就定3秒,代码我们就改成了如下样子

var numFlow = flow 
    delay(3000)
    emit("喜羊羊")
    emit("美羊羊")
.stateIn(lifecycleScope, SharingStarted.Lazily, "灰太狼")
bindingView.stateflowBtn.setOnClickListener 
    GlobalScope.launch(Dispatchers.Main) 
        launch 
            numFlow.collect 
                println("订阅者1获取值 $it")
            
        
        launch 
            numFlow.collect 
                println("订阅者2获取值 $it")
            
        
    

重新运行一下代码后我们点击按钮,输出的结果就变成了

 I  订阅者1获取值 灰太狼
 I  订阅者2获取值 灰太狼
 I  订阅者1获取值 美羊羊
 I  订阅者2获取值 美羊羊

在等待3秒的过程中默认值就被发送出来了,后面3秒过后真正的数据美羊羊也被发送了出来,简单的模拟了一个在请求数据过程中从空态页到真正数据展示在屏幕上的过程

Flow与LiveData

记得Flow刚被推出来的那一段时间,经常会看到一些文章或者评论有说到LiveData将要被取代,那LiveData究竟会不会被取代呢,我们还是来总结下LiveData的优缺点

优点

  1. 职责单一,主要作用就是在主线程更新UI,上手简单,可满足于目前大部分需求
  2. 目前国内还有不少项目以java为主,而Flow是kotlin语言在协程环境下使用的工具,所以LiveData对于纯java项目来说还是必不可少的

缺点

  1. 在异步线程中使用postValue容易丢失数据
  2. LiveData会产生诸如数据倒灌的问题

总结

个人感觉在今后的技术领域里面,所有新出来的技术并不是以取代谁为目的而推出来的,而是在这个日趋复杂的业务环境中,让我们开发者多一个技术方案的选择,选择合适自己以及项目的方案,比如LiveData的职责单一上手容易,Flow丰富的操作符,以及配合协程让业务功能实现起来更容易,后面也会尝试着用Flow代替RxJava写一个网络请求框架,也算是对Flow的一个真正实践

作者:Coffeeee
链接:https://juejin.cn/post/7185324007007191095

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。


相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

以上是关于Kotlin系列之认识一下Flow的主要内容,如果未能解决你的问题,请参考以下文章

认识一下Kotlin语言,Android平台的Swift

KSP - 元编程编译提速的小助手

Android SingleLiveEvent Redux with Kotlin Flow

深潜Kotlin协程(十九):Flow 概述

深潜Kotlin协程(十九):Flow 概述

深潜Kotlin协程(二十):构建 Flow