响应式编程
官方用小牛去湖边打水的例子来解释:
非响应式:用水桶去湖边装水,然后再抬回家
响应式:把水管连接到湖边,需要用水的时候打开水龙头
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后会重新订阅。