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后会重新订阅。

相关推荐
Devil枫2 小时前
Kotlin高级特性深度解析
android·开发语言·kotlin
ChinaDragonDreamer2 小时前
Kotlin:2.1.20 的新特性
android·开发语言·kotlin
雨白12 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹14 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空16 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭16 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日17 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安17 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑17 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟21 小时前
CTF Web的数组巧用
android