kotlin协程之Flow

响应式编程

官方用小牛去湖边打水的例子来解释:

非响应式:用水桶去湖边装水,然后再抬回家

响应式:把水管连接到湖边,需要用水的时候打开水龙头

Kotlin中的Sequence

序列(Sequence)实际上是对应 Java8 中的 Stream

看一个例子:

普通集合操作:

kotlin 复制代码
    fun listSample(){
        val currentTimeMillis = System.currentTimeMillis()
        val list = (0..10000000).filter { it in 101..199 }.map {
            "第${it}个"
        }.take(10)
        LogUtil.e(System.currentTimeMillis()-currentTimeMillis)
        LogUtil.e(list)
    }

序列操作:

kotlin 复制代码
     fun sequenceSample(){
        val currentTimeMillis = System.currentTimeMillis()
        val list = (0..10000000).asSequence().filter { it in 101..199 }.map {
            "第${it}个"
        }.take(10).toList()
        LogUtil.e(System.currentTimeMillis()-currentTimeMillis)
        LogUtil.e(list)
    }

可以看到最终结果是一样的,但是耗时差了很多,普通集合操作用了748毫秒,而序列操作仅用时2毫秒

普通集合操作:每一次操作都会产生新的中间结果

序列操作:惰性求值,在进行中间操作的时候,是不会产生中间数据结果的,只有等到进行末端操作的时候才会进行求值。

上面的例子中,普通集合操作时流程:

序列操作时:

序列只有等到进行末端操作才会启动,如果上面的例子中,把.toList()去掉,整个流程将不会进行操作

Flow

flow-流,数据流,数据从生成的地方流到订阅的地方,订阅的时候就相当于打开水龙头。

相当于可以切换线程和有更多操作符的Sequence

创建Flow

1.使用flow{}创建

kotlin 复制代码
 fun useFlow(): Flow<String> {

        val flow= flow<String> {
            LogUtil.e("flow{}创建流")
            emit("登录中")
            delay(1000)
            emit("校验中")
            delay(1000)
            emit("即将登录成功")
            delay(1000)
            emit("登录成功")
        }

        return flow
    }

2.flowOf

3.asFlow

订阅Flow

上面创建的方法如果只是调用了useFlow,flow{}里面是不会执行的,和Sequence一样,只有末端操作,flow才会执行,而订阅就是一种末端操作

1.collect

kotlin 复制代码
            lifecycleScope.launch {
                viewModel.useFlow().collect{
                    LogUtil.e(it)
                }
            }

2.collectLatest

如果使用collect的时候,在订阅的地方有耗时操作,并且耗时比生成数据的间隔要更多,那么订阅的数据将不是最新的,使用collectLast可以保证有新数据能在订阅出及时收到。

修改一下上面订阅的地方,延迟2000毫秒后再打印一次:

kotlin 复制代码
            lifecycleScope.launch {
                viewModel.useFlow().collect{
                    LogUtil.e(it)
                    delay(2000)
                    LogUtil.e(it)
                }
            }

虽然每次都打印了,但是时间间隔比生产数据的地方1000毫秒要多,说明拿到的不是最新的数据

把collect改成collectLatest:

kotlin 复制代码
                viewModel.useFlow().collectLatest{
                    LogUtil.e(it)
                    delay(2000)
                    LogUtil.e(it)
                }

可以看到,有新的数据时,会把当前订阅没处理完的流程取消,直接处理新的数据

切换线程

flowOn方法用于将上游的流切换到指定协程上下文的调度器中执行,同时不会把协程上下文暴露给下游的流,即flowOn方法中协程上下文的调度器不会对下游的流生效。

kotlin 复制代码
fun flowOnSample(): Flow<String> {
        return flow {
            LogUtil.e("flow{}--${Thread.currentThread().name}")
            emit("登录中")
        }.flowOn(Dispatchers.IO).map {
            LogUtil.e("map--${Thread.currentThread().name}")
            it
        }.flowOn(Dispatchers.Main).filter {
            LogUtil.e("filter--${Thread.currentThread().name}")
            it!="校验中"
        }.flowOn(Dispatchers.IO)
    }
kotlin 复制代码
            val flowThreadSample = viewModel.flowOnSample()
            lifecycleScope.launch {
                flowThreadSample.collectLatest {
                    LogUtil.e(Thread.currentThread().name)
                }
            }

打印情况如下:

注:订阅处的线程永远都是订阅时所在协程指定的线程。

更多操作符

可以查看郭霖的文章

冷流和热流

  • 冷流:只有当订阅者发起订阅时,事件的发送者才会开始发送事件。
  • 热流:不管订阅者是否存在,flow本身可以调用emit(或者tryEmit)发送事件,可以有多个观察者,也可在需要的时候发送事件。 从描述看,SharedFlow更接近于传统的观察者模式。

StateFlow和SharedFlow

  • 事件(Event): 事件是一次有效的,新订阅者不应该收到旧的事件,因此事件数据适合用 SharedFlow(replay=0);
  • 状态(State): 状态是可以恢复的,新订阅者允许收到旧的状态数据,因此状态数据适合用 StateFlow。

如显示在界面的用户名应该用StateFlow,而登录时错误的提示信息应该用SharedFlow:

kotlin 复制代码
val userName= MutableStateFlow("周杰伦")
val loginErrorMessage= MutableSharedFlow<String>()

调用订阅后会阻塞

以下代码只会订阅userName,因为订阅之后就会一直阻塞:

kotlin 复制代码
lifecycleScope.launch {
     viewModel.userName.collectLatest {
                LogUtil.e(it)
                viewBinding.tvUsername.text=it
     }
     LogUtil.e("这里永远不会执行")
     viewModel.loginErrorMessage.collectLatest {
         Toast.makeText(this@FlowActivity,it, Toast.LENGTH_SHORT).show()
     }
}

可以改成这样,在子协程里面订阅:

kotlin 复制代码
lifecycleScope.launch {
     launch {
          viewModel.userName.collectLatest {
                    LogUtil.e(it)
                    viewBinding.tvUsername.text=it
          }
     }
     launch {
         viewModel.loginErrorMessage.collectLatest {
            Toast.makeText(this@FlowActivity,it, Toast.LENGTH_SHORT).show()
         }
     }
}

界面相关请用repeatOnLifecycle去订阅

如果需要更新界面,切勿使用 launch函数从界面直接收集数据流。即使 View 不可见,这些函数也会处理事件。此行为可能会导致应用崩溃。

kotlin 复制代码
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED){
                launch {
                    viewModel.userName.collectLatest {
                        LogUtil.e(it)
                    }
                }
                launch {
                    viewModel.loginErrorMessage.collectLatest {
                       LogUtil.e(it)T).show()
                    }
                }
            }
        }

上面的代码当Activity的生命周期不处于STARTED时,订阅将被取消,当恢复到STARTED后会重新订阅。

相关推荐
服装学院的IT男4 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms2064 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男4 小时前
【Android 源码分析】Activity生命周期之onStop-1
android
人间有清欢6 小时前
十、kotlin的协程
kotlin
吾爱星辰6 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer6 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
网络研究院9 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下9 小时前
android navigation 用法详细使用
android
小比卡丘11 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭12 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android