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

相关推荐
枯骨成佛43 分钟前
Android中Crash Debug技巧
android
kim56596 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼6 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ6 小时前
Android Studio使用c++编写
android·c++
csucoderlee7 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim56597 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式7 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。7 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio
ROCKY_8178 小时前
AndroidStudio-滚动视图ScrollView
android
趴菜小玩家9 小时前
使用 Gradle 插件优化 Flutter Android 插件开发中的 Flutter 依赖缺失问题
android·flutter·gradle