RxJava VS kotlin flow

news2024/11/28 22:54:46

1.基础概念介绍

1.1 观察者模式

观察者模式,其实对于Android开发者而言,并不陌生,button的setOnClickListener,就是一个典型的观察者模式。控件button是被观察者,它产生一个事件(点击),观察者OnClickListener接收到,做出相应的处理,而setOnClickListener就是订阅者,它将两者连接起来

以上面为例,观察者模式需要具备的三个角色:被观察者,观察者,事件,一个动作:订阅

角色作用类别
被观察者(Observable)产生事件控件
观察者(Observer)接收事件,并给出响应动作OnClickListener
订阅(Subscribe)连接 被观察者 & 观察者setOnClickListener
事件(Event)被观察者 & 观察者 沟通的载体控件被点击

1.2 函数响应式编程

在第一次接触这些词的时候,我满脑子的问号,看官方文档也是云山雾里的。后来我翻看了各种文档,说一下我对这些词的理解:

  • 响应式编程(Reactive Programming RP):万物皆可视为数据流,比如:用户的操作,网络数据,某个代码状态变更

  • 函数式编程(Functional Programming FP):将逻辑抽象为旧数据如何映射为新数据。(比如rx或者kotlin里的各种操作符)

  • 函数响应式编程(Reactive Functional Programming RFP): 将数据流的各种操作(创建,结合,过滤等)抽象为函数(操作符),使得数据流可以自由的组合这些函数实现各种数据流的映射

推荐文章:

  • 那些年我们错过的响应式编程

  • 什么是函数式编程思维

1.3 Rxjava

ReactiveX是Reactive Extensions的缩写,一般简写为Rx,Rx是一个编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流。Rx的大部分语言库由ReactiveX这个组织负责维护,比较流行的有RxJava/RxJS/Rx.NET,社区网站是 reactivex.io。

ReactiveX.io给的定义是,Rx是基于观察者模式的实现了异步编程接口,ReactiveX结合了观察者模式、迭代器模式和函数式编程的精华。Rxjava则是Reactive Extensions的java实现

简单说,Rxjava是函数响应式编程思想的一种体现,基于观察者模式实现的一种异步编程接口

原理是: 被观察者 (Observable) 通过 订阅(Subscribe) 按顺序发送数据 给观察者 (Observer), 观察者(Observer) 按顺序接收事件 & 作出对应的响应动作。

image

特点:

  • 操作符丰富

  • 支持背压

  • 支持线程切换操作

  • 学习成本和上手难度较高

1.4 Flow

Flow是kotlin提供的一个工具( kotlinx 包下的组件),它也是函数响应式编程(RFP)思想的一种体现,也是使用的观察者模式,但拥有尽可能简单的设计, 对 Kotlin 以及协程友好且遵从结构化并发。

特点:

  • 支持线程(协程)

  • 跟协程绑定的比较多

  • 支持背压

  • 操作符和rx整体来说差不多

  • 学习成本低(前提是得会协程)

1.5 LiveData

LiveData 是 androidx 包下的组件,是 Android 生态中一个的简单的生命周期感知型容器。与常规的可观察类不同,LiveData 具有生命周期感知能力,它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。
这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。

特点:

  • 可以感知生命周期

  • 只能在主线程更新数据

  • 不支持背压

  • 不支持防抖

  • 没有操作符

关于更多LiveData的使用和源码,可以看我之前的文章: JetPack框架组件2——liveData的使用和源码分析

2.Hello Word

同button的setOnClickListener一样,RxJava和kotlin flow 都是基于观察者模型实现的编程接口。所以和button的setOnClickListener使用类似,即:

  • 实现被观察者(button)

  • 实现观察者(OnClickListener)

  • 注册(setOnClickListener)

  • 被观察者发出事件,观察者接受到(点击)

2.1 Hello Rxjava

实现被观察者observable

// 1. 创建被观察者 Observable 对象
val observable = Observable.create( object : ObservableOnSubscribe<String> {
    // create() 是 RxJava 最基本的创造事件序列的方法

    //2.在复写的subscribe()里定义需要发送的事件
    override fun subscribe(emitter: ObservableEmitter<String>) {
        // 通过 ObservableEmitter类对象产生事件并通知观察者

        // ObservableEmitter类介绍
        // a. 定义:事件发射器
        // b. 作用:定义需要发送的事件 & 向观察者发送事件
        emitter.onNext("hello rxjava")//发送事件
        emitter.onComplete()//发送完成事件
    }
})

实现观察者observer

// 1. 创建观察者 (Observer )对象
val observer =  object : Observer<String> {
    // 2. 创建对象时通过对应复写对应事件方法 从而 响应对应事件

    override fun onSubscribe(d: Disposable) {
        Log.d(TAG, "开始采用subscribe连接")
    }

    override fun onNext(t: String) {
        Log.d(TAG, "对Next事件作出响应: $t")
    }

    override fun onError(e: Throwable) {
        Log.d(TAG, "对Error事件作出响应")
    }

    override fun onComplete() {
        Log.d(TAG, "对Complete事件作出响应")
    }
}

注册

observable.subscribe(observer)

执行结果

image

原理

这一块源码的实现,感兴趣可以看我之前的博客:Android之Rxjava2.X 9————Rxjava源码阅读1

其实Rxjava上面部分源码的实现很简单,一句话概括

使用Observable.create创建时,需要传入的ObservableOnSubscribe接口中的subscribe方法参数,这个参数是用来发射数据.它其实是Observer的包装类

源码核心逻辑

public final class ObservableCreate<T> extends Observable<T> {
    @Override
    protected void subscribeActual(Observer<? super T> observer) {
        CreateEmitter<T> parent = new CreateEmitter<T>(observer);
        observer.onSubscribe(parent);

        try {
            //source即为ObservableOnSubscribe
            source.subscribe(parent);
        } catch (Throwable ex) {
            Exceptions.throwIfFatal(ex);
            parent.onError(ex);
        }    
    }    
}

通过源码也可以看出来,当完成observable.subscribe(observer)时,observable才可以发送数据

更简洁的写法

Observable
    .just("hello rxjava") //构造简单的Observable方法
    .subscribe { Log.d(TAG, "$it") } //可以只实现onNext方法
    
    //日志:hello rxjava

2.2 Hello Flow

实现被观察者flow

val flow: Flow<String> = flow { // 流构建器
    emit("hello flow") // 发送下一个值
}

实现观察者flowCollector

val flowCollector: FlowCollector<String> = FlowCollector {
    Log.d(TAG, "$it")
}

注册

注意,flow是存在suspend标识,所以必须运行在协程中

GlobalScope.launch{
    flow.collect(flowCollector)
}

执行结果

image

原理

看了flow的原理,其实和rxjava类似。flow创建,传入的FlowCollector接口,其实也是flowCollector进行包装后的对象

public abstract class AbstractFlow<T> : Flow<T>, CancellableFlow<T> {

    public final override suspend fun collect(collector: FlowCollector<T>) {
        val safeCollector = SafeCollector(collector, coroutineContext)
        try {
            collectSafely(safeCollector)
        } finally {
            safeCollector.releaseIntercepted()
        }
    }
 }

一般写法

flow {
    emit("hello flow") // 发送下一个值
}.collect{
    Log.d(TAG, "$it")
}
//日志: MainActivity: hello flow

是否可以类似Rxjava那也,也可以发射开始,发射结束,error等状态的回调。这个是可以的

  • onStart:数据流开始发射

  • onEach:数据流中的每一个数据发射时的回调

  • onCompletion:数据流结束发射

  • onEmpty:当数据流中没有发射任何数据时

  • catch: 发生异常

flow {
    emit(1)
    emit(2)
    emit(3)
    emit(4)
}.catch { e ->
    // 发生了异常。显示异常信息
    Log.d(TAG, "加载错误: $e")
}.onEmpty {
    // 空白数据
    Log.d(TAG,"什么数据都没有")
}.onStart {
    Log.d(TAG,"正在加载中")
}.onEach {
    Log.d(TAG,"开始处理 $it")
}.onCompletion { e->
    if(e == null){
        Log.d(TAG,"加载结束 $it")
    }else{
        Log.d(TAG,"加载失败 $e")
    }

}.collect {
    Log.d(TAG,"加载成功 $it")
    println(it)
}

3.进阶

3.1 线程切换

3.1.1 Rxjava的线程切换

Rxjava的线程操作功能符号有两个:

  • subscribeOn() 指定被观察者的线程,有一点需要注意就是如果多次调用此方法,只有第一次有效。

  • observerOn() 指定观察者的线程,每指定一次就会生效一次。

subscribeOn

Observable
    .create<Int> {
        Log.e(TAG, "threadName:" + Thread.currentThread().getName());
        it.onNext(1);
    }.subscribeOn(Schedulers.newThread())
    .subscribe { Log.d(TAG, "$it") }

image

observerOn()

Observable.fromIterable(listOf(0, 1, 2, 3, 4)) //根据list快速创建一组Observable
    .map { //转化操作符
        Log.d(TAG, "subscribeOn threadName: ${Thread.currentThread().name} it = $it"  )
        return@map it * 10
    }.observeOn(AndroidSchedulers.mainThread())
    .subscribeOn(Schedulers.newThread())
    .subscribe {
        Log.d(TAG, "observerOn threadName: ${Thread.currentThread().name}")
        Log.d(TAG, "onNext: $it")
    }

image

3.1.2 flow的线程切换

flow的线程操作符只有flowOn

flowOn介绍:

  • 操作符对上游范围有效, 范围是指两个flowOn之间, 如果只有一个flowOn,则上游全部有效

  • 最后一个flowOn后的操作所在线程与当前整个flow所在的线程池相同

(0..4).asFlow()
    .onStart { Log.d(TAG, "flow start threadName: ${Thread.currentThread().name}"  )}
    .flowOn(Dispatchers.IO)
    .map {
        // 运行在 dispatcher2
        Log.d(TAG, "map threadName: ${Thread.currentThread().name} it = $it"  )
        it * 10
    }
    .flowOn(Dispatchers.Main)
    .collect {
        // 运行的协程取决于整个 flow 在哪个协程调用
        Log.d(TAG, "collect ${Thread.currentThread().name} it = $it"  )
        println(it)
    }

image

3.2 背压

3.2.1 背压介绍

针对的场景:

Observable.create(ObservableOnSubscribe<Int> {
    var i = 0
    while (true) {
        i++
        it.onNext(i)
    }
})
.subscribeOn(Schedulers.newThread())
.observeOn(Schedulers.newThread())
.subscribe {
    Thread.sleep(5000);
    Log.d(TAG, "onNext: $it")
}

在异步订阅的(比如网络请求),被观察者发生事件的速度太快,观察者来不及接受所有的事件,从而缓存区中的事件越积越多,最终导致缓存区溢出,事件丢失并OOM

image

定义

Backpressure,也称为Reactive Pull,就是下游需要多少(具体是通过下游的request请求指定需要多少),上游就发送多少。

作用

在异步场景中,被观察者发送事件速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略

背压策略的原理

  • 对于观察者:响应式拉取,即观察者根据自己的实际需求接受事件

  • 对于被观察者:反馈控制,即被观察者根据观察者的接受能力,从而控制发送事件的速度

  • 对于缓存区:对超出缓存区大小的事件进行丢弃,保留,报错。

3.2.2 Rxjava中背压的使用

在Rxjava中,如果需要使用背压,需要使用Observable(被观察者)的另一种实现:Flowable。

Flowable的特点:

  • 对应的观察者变为Subscribe

  • 所有操作符强制支持背压

  • 默认的缓存区的大小为:128。缓存区使用队列存放事件

Flowable的能力

  • request()可以控制observer(观察者)接受事件的速度

  • requested()可以控制Observable(被观察者)发送事件的速度

  • 背压策略:处理缓存区的逻辑

下面以异步订阅为例,讲解这三个能力的使用,更详细的使用以及同步订阅的处理,参考我的博文

Android之Rxjava2.X 8————Rxjava 背压策略

request()

@SuppressLint("CheckResult")
fun backpressure(){
    Flowable.create(FlowableOnSubscribe<Int> {
        Log.d(TAG, "发送事件 1");
        it.onNext(1);
        Log.d(TAG, "发送事件 2");
        it.onNext(2);
        Log.d(TAG, "发送事件 3");
        it.onNext(3);
        Log.d(TAG, "发送事件 4");
        it.onNext(4);
        Log.d(TAG, "发送完成");
        it.onComplete();
    },BackpressureStrategy.ERROR)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(object : Subscriber<Int> {
            override fun onSubscribe(s: Subscription?) {
                s?.request(3)
            }

            override fun onNext(t: Int?) {
                Log.d(TAG, "接收到了事件 $t");
            }

            override fun onError(t: Throwable?) {
                Log.d(TAG, "onError $t");
            }

            override fun onComplete() {
                Log.d(TAG, "onComplete ");
            }

        })

image

注意点:

  1. 如果未设置request,则默认未接收

  2. 观察者不接收事件的情况下,被观察者继续发送事件 & 存放到缓存区,再按需求取出

  3. 超出缓存区的数据,会按照策略进行处理

requested()

异步订阅下,**requested()**反向控制Observable

image

Flowable.create(
    FlowableOnSubscribe<Int> {
        // 被观察者一共需要发送500个事件
        for (i in 0..500) {
            var flag = false;
            // 若requested() == 0则不发送
            while (it.requested() == 0L) {
                if (!flag) {
                    Log.d(TAG, "不再发送");
                    flag = true;
                }
            }

            // requested() ≠ 0 才发送
            Log.d(TAG, "发送了事件" + i + ",观察者可接收事件数量 = " + it.requested());
            it.onNext(i);
        }
    },
    BackpressureStrategy.ERROR)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(object : Subscriber<Int> {
        override fun onSubscribe(s: Subscription?) {
            subscription = s
        }

        override fun onNext(t: Int?) {
            Log.d(TAG, "接收到了事件 $t");
        }

        override fun onError(t: Throwable?) {
            Log.d(TAG, "onError $t");
        }

        override fun onComplete() {
            Log.d(TAG, "onComplete ");
        }

    })

image

image

image

背压策略

目前rxjava有5种策略

  • BackpressureStrategy.ERROR 超出缓存区,抛出异常

  • BackpressureStrategy.MISSING 提示缓存区满了

  • BackpressureStrategy.BUFFER 缓存区大小设置成无限大

  • BackpressureStrategy.DROP 超过缓存区大小(128)的事件丢弃

  • BackpressureStrategy.LATEST 只保存最新(最后)事件,超过缓存区大小(128)的事件丢弃(即如果发送了150个事件,缓存区里会保存129个事件(第1-第128 + 第150事件))

3.2.3 flow中背压的使用

flow的操作符很简单,使用一个操作符解决:

buffer(capity, onBufferOver)

  • capity 缓存的大小

  • onBufferOverflow 缓存超出的策略

同时给予buffer衍生了一系列背压相关的操作符

  • conflate(): 只取最新的数据,等价 buffer(0, DROP_OLDEST),即不缓存数据,直接取最新数据的处理

  • collectLatest():类似conflate,但是不会直接用新数据覆盖老数据,而是每一个都会被处理,只不过如果前一个还没被处理完后一个就来了的话,处理前一个数据的逻辑就会被取消。

  • mapLatest:同理 collectLatest

  • flatMapLatest:同理 collectLatest

flow {
    (1..500).forEach {
        delay(100)
        println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
        emit(it)
    }
}
    .buffer(capacity = 128, onBufferOverflow = BufferOverflow.DROP_OLDEST)
    .collect {
        delay(500)
        Log.d(TAG, "接收到了事件 $it");
    }

采用了DROP_OLDEST策略,此时当缓冲区超出处理速度时,就会丢弃老数据

image

3.3 flow拓展

冷流:即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才从开始产生数据。

热流:即无论下游是否有消费行为,上游都会自己产生数据

3.3.1一般flow

一般的flow,仅有一个观察者。冷流

//构建
val testFlow = flow<String>{
    emit("hello")
    emit("flow")
}

//接收
coroutineScope.launch{
    testFlow.collect{ value->
        println(value)
    }
}

//打印
hello
flow

3.3.2 stateFlow

  • 有状态的FLow 可以有多个观察者,热流

  • 需要时传入初始值, initialState

  • 常用作与UI相关的数据观察,类比LiveData

//创建
val uiState=MutableStateFlow(Result.Loading)

//监听
coroutineScope.launch{
    uiState.collect{ value->
        println(value)
    }
}

//赋值
uiState.value=Result.Sucess

//打印结果
Result.Loading
Result.Sucess

3.3.3 sharedFlow

  • 升级版StateFloe,有多个观察者,热流,无需初始值

有三个参数值

  • replay - 重播给新订阅者的值的数量(不能为负,默认为零)

  • extraBufferCapacity - 除了replay之外缓冲的值的数量。 当有剩余缓冲区空间时, emit不会挂起(可选,不能为负,默认为零)

  • onBufferOverflow - 配置缓冲区溢出的操作(可选,默认为暂停尝试发出值)

//创建
val signEvent=MutableSharedFlow <String> ()

//监听
coroutineScope.launch{
    signEvent.collect{ value->
        println(value)
    }
}
//赋值
signEvent.tryEmit("hello")
signEvent.tryEmit("shared flow")

//打印结果
hello
shared flo

3.4 更多操作符

3.4.1 RxJava操作符系列

Rxjava最大的优势就是种类繁多的操作符。常用的具体如下,这里只进行列举功能,详细的可以参考后面的文档:

  • 创建操作 Android之Rxjava2.X 2————Rxjava 创建操作符

    • create:Create操作符创建一个完整的Observable,可以传递onNext,onError和onCompleted等事件
    • just: 根据传入的参数,快速创建一个Observable
    • fromArray :根据传入的数组,快速创建一个Observable
    • fromIterable: 根据传入的集合,快速创建一个Observable
    • empty:仅发送Complete事件,直接通知完成。error:仅发送Error事件,直接通知异常。never:不发送任何事件
    • defer: 发生订阅时,才创建Observable
    • timer:延迟给定时间后,创建Observable
    • Interval: 创建一个按固定的时间间隔发射一个无限递增的整数序列的Observable
    • intervalRange: 创建一个按固定的时间间隔发射一个给定的事件序列范围的Observable
    • range /rangeLong: 创建一个发射给定的事件序列范围的Observable
  • 变换操作 Android之Rxjava2.X 3————Rxjava 变换操作符

    • map: 根据传入的函数,对Observable发射的每一项数据处理转化
    • flatMap:将Observable发射的每一个数据转化成一个新的Observable。注意这个过程可能是无序的
    • concatMap:类似于flatMap,但它是可以保证有序
    • Buffe: 被观察者(Obervable)需要发送的事件中, 获取一定数量的事件放到缓存区中,最终发送
  • 组合操作 Android之Rxjava2.X 4————Rxjava 组合操作符

    • concat/concatArray: 组合多个被观察者一起发送数据,合并后 按发送顺序串行执行
    • merge/mergeArray :组合多个被观察者一起发送数据,合并后 按时间线并行执行
    • zip:合并多Observable,并根据BiFunction函数将多个Observable的值生成一个新的值发射出去。注意zip是按照个数合并
    • combineLatest: 合并多Observable,并根据BiFunction函数将多个Observable的值生成一个新的值发射出去。combineLatest是按照时间线合并
    • concatDelayError / mergeDelayError/combineLatestDelayError: 将onError()事件延迟到所有Observable都发送完事件后再执行
    • reduce:将Observable的所有事件聚合为1个事件
    • collect:将Observable的所有事件聚合到一个数据结构中(list)
    • startWith / startWithArray:在一个被观察者发送事件前,追加发送一些数据 / 一个新的被观察者
    • count:统计被观察者发送事件的数量
  • 过滤操作 Android之Rxjava2.X 5————Rxjava 过滤操作符

    • filter:筛选符合要求的事件。返回true则发送事件,否则不会发送
    • ofType:类似filter,它可以让Observable只返回指定类型的数据。
    • skip/skipLast: 只返回前n/后n个数据
    • distinct/ distinctUntilChanged:过滤事件序列中重复的事件 / 连续重复的事件
    • take/takeLast:只发射前面/后面的N项数据
    • throttleFirst:/ throttleLast(sample): 在某段时间内,只发送该段时间内第1次事件 / 最后1次事件
    • sample:在某段时间内,只发送该段时间内最新(最后)1次事件
    • throttleWithTimeout /debounce: 发送数据事件时,若2次发送事件的间隔<指定时间,就会丢弃前一次的数据
    • firstElement / lastElement/elementAt: 仅选取第1个元素 / 最后一个元素/指定位置
  • 功能 Android之Rxjava2.X 6————Rxjava 功能操作符

    • subscribeOn/observerOn 线程切换
    • delay:延迟发送
    • doxx:在事件的某个生命周期中调用(doOnEach/doOnNext/…)
    • onErrorReturn/onErrorResumeNext /onExceptionResumeNext:遇到错误时,发送1个新的Observable/中止发送
    • retry/retryUntil/retryWhen:重试,即当出现错误时,让被观察者(Observable)重新发射数据
    • repeat/repeatWhen:重新发送
  • 条件操作符:Android之Rxjava2.X 7————Rxjava 条件操作符

    • all:判定是否Observable发射的所有数据都满足某个条件
    • takeWhile:发射Observable发射的数据,直到一个指定的条件不成立
    • skipWhile:丢弃Observable发射的数据,直到一个指定的条件不成立
    • takeUntil:执行到某个条件时,停止发送事件
    • skipUntil:等到 skipUntil() 传入的Observable开始发送数据,(原始)第1个Observable的数据才开始发送数
    • SequenceEqual:判定两个Observables是否发射相同的数据序列。
    • contains:判断发送的数据中是否包含指定数据
    • isEmpty:判断发送的数据是否为空
    • amb:当需要发送多个 Observable时,只发送 先发送数据的Observable的数据,而其余 Observable则被丢弃。
    • defaultIfEmpty:在不发送任何有效事件( Next事件)、仅发送了 Complete 事件的前提下,发送一个默认值

3.4.2 flow操作符系列

我发现flow的操作符,基本上可以替代RxJava,也提供了诸多操作符来处理数据

具体使用和示例参考博客:https://juejin.cn/post/6989536876096913439

  • 创建flow

    • flow:创建Flow
    • flowOf:快速创建 flow
    • asFlow:将其他数据转换成 普通的flow ,一般是集合向Flow的转换
    • callbackFlow:将回调方法改造成flow
    • emptyFlow:返回一个空流
    • channelFlow:创建一个允许在构造代码块中切换线程的flow
  • 末端操作,在flow最后调用,此时返回的不是一个flow了

    • collect:触发flow的运行 。 通常的监听方式
    • collectIndexed:带下标的collect
    • collectLatest:与 collect的区别是 ,有新值发出时,如果此时上个收集尚未完成,则会取消掉上个值的收集操作(之后xxxlatest区别都是这个)
    • toCollection/toList/toSet:将结果添加到集合/list/set
    • launchIn:直接触发流的执行
    • last/lastOrNull /first/firstOrNull/single/singleOrNull:返回流 发出 的最后/第一个值
    • count:返回流发送值的个数
    • fold/reduce:从初始值开始(reduce无)执行遍历,并将结果作为下个执行的 参数。
  • 变换操作

    • map/mapLatest/mapNotNull:将发出的值 进行变换
    • transform/transformLatest/transformWhile:对发出的值 进行变换 ,区别于map, transform的接收者是FlowCollector
    • asStateFlow/asSharedFlow: 将 MutableSharedFlow 转换为 StateFlow/SharedFlow
    • receiveAsFlow/consumeAsFlow:将Channel 转换为Flow
    • withIndex:将结果包装成IndexedValue 类型
    • scan/runningFold/runningReduce:和 fold 相似,区别是fold 返回的是最终结果,scan返回的是个flow ,会把初始值和每一步的操作结果发送出去。
    • shareIn/stateIn:将普通flow 转化为 SharedFlow/StateFlow
  • 组合操作符

    • zip:对两个流进行组合,分别从二者取值,一旦一个流结束了,那整个过程就结束了。
    • combine:组合每个流最新发出的值。
    • merge:合并多个流为 一个流
    • flattenConcat/flattenMerge:以顺序方式将给定的流展开为单个流 ,flattenMerge可以设置并发收集流的数量。
    • flatMapContact /flatMapLatest:这是一个组合操作符,相当于 map + flattenConcat , 通过 map 转成一个流,在通过 flattenConcat
    • flatMapMerge:也是组合操作符,简化使用。 map + flattenMerge
  • 过滤操作符

    • filter/filterNot/filterNotNull:筛选出符合条件的值
    • filterInstance:筛选对应类型的值
    • drop:作用是 丢弃掉前 n 个的值
    • dropWhile:找到第一个不满足条件的,返回其和其之后的值。
    • take:返回前 n个 元素
    • takeWhile:也是找第一个不满足条件的项,但是取其之前的值
    • debounce:防抖节流
    • sample:给定一个时间周期,仅获取周期内最新发出的值
    • distinctUntilChangedBy/distinctUntilChanged:去重操作符,判断连续的两个值是否重复
  • 功能操作符

    • cancellable :接收的的时候判断 协程是否被取消 ,如果已取消,则抛出异常
    • catch:对上游异常进行捕获 ,对下游无影响
    • retryWhen:有条件的进行重试
    • retry:流发生异常时可以重新执行
    • buffer:处理背压
    • conflate:仅保留最新值
    • flowOn:指定上游操作的执行线程

4.牛刀小试

4.1联合判断&点击防抖

模拟登陆场景描述:

  • 两个EditText,输入账号和密码。一个Button,点击发起登录

  • 只有两个EditText有输入,此时button从灰色变成蓝色,并且可以点击

  • button点击要具有防抖功能(1s内只有第一次算有效点击)

需求分析

联合判断: 只有当账号和密码都有输入时,才可以点击

  • 将账号和密码的输入视为两个数据流,当两个数据流都符合要求时,产生一个新的数据流表示是否可点

  • 可以考虑使用组合操作符:combineLatest(combine)。将两个EditText的产生的字符流按照时间线,合并为一个表示是否可以点击的数据流

button的防抖功能

  • 将button的点击视为数据流规定时间周期之内,只接受第一个事件

  • 可以考虑过滤操作符: throttleFirst(sample)在1s内只接受第一个事件,或者直接使用sample(debounce)防抖操作符

同时,这里引入了一个新的问题,如何将Android UI操作转化为数据流

4.1.1 Rxjava实现

如果将View的操作转化为Observable,可以直接使用现有的第三方库(但tt好像没有引入)

compile ‘com.jakewharton.rxbinding:rxbinding:0.4.0’

自己实现一个View操作转化为Observable,参考ObservableCreate

将button点击转化为Observable(其他逻辑的需要转化成Observable可以参考)

class ViewClickObservale(private val view: View) : Observable<View>() {

    override fun subscribeActual(observer: Observer<in View>) {
        val listener = Listener(view, observer)
        observer.onSubscribe(listener)
        view.setOnClickListener(listener)
    }

    internal class Listener(private val view: View, private val observer: Observer<in View>) :
        MainThreadDisposable(), View.OnClickListener {

        override fun onClick(v: View) {
            if (!isDisposed) {
                observer.onNext(v)
            }
        }
        override fun onDispose() {
            view.setOnClickListener(null)
        }
    }
}

完成需求:

@SuppressLint("CheckResult")
private fun logOnRxjava() {

    val account: EditText = findViewById (R.id.account)
    val password: EditText = findViewById (R.id.password)
    val logOn: Button = findViewById (R.id.log_on)
    logOn.isEnabled = false

    //使用RxBinding将EditText将textChanges转化为Observable
    //skip跳过 一开始EditText无任何输入时的空值
    val accountObservable =  RxTextView.textChanges(account).skip(1)
    val passwordObservable =  RxTextView.textChanges(password).skip(1)

    //使用combineLatest将事件合并 两个EditText的输入-->button是否可点
    Observable.combineLatest(accountObservable,passwordObservable) { _, _ ->
       account.text.isNotEmpty() && password.text.isNotEmpty()
    }.subscribe{
        logOn.isEnabled = it
    }

    //使用自定义的Observale,将view的点击转化为Observale
    ViewClickObservale(logOn) 
        .throttleFirst(1, TimeUnit.SECONDS)//根据时间,进行防抖
        .subscribe{
            Toast.makeText(this,"正在登录...",Toast.LENGTH_LONG).show()
        }
}

实现效果:

此处应该有视频,但掘金不支持视频

4.1.2 flow实现

将ui流数据转化为flow,因为ui在主线程,而flow的collect必须在协程中调用,所以使用channelFlow创建flow。更多可以参考文章:使用更为安全的方式收集 Android UI 数据流

private fun viewClickFlow(view: View): Flow<View?> = channelFlow {
    view.setOnClickListener {
        trySend(view)
    }
}

private fun viewTextChangeFlow(view: TextView):Flow<CharSequence?> = channelFlow {
    val callback = object : TextWatcher{
        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            trySend(p0)
        }
        override fun afterTextChanged(p0: Editable?) {}
    }

    view.addTextChangedListener(callback)

    awaitClose {
        view.removeTextChangedListener(callback)
    }
}

需求实现:

    private fun logOnFLow(){

        val account: EditText = findViewById(R.id.account)
        val password: EditText = findViewById(R.id.password)
        val logOn: Button = findViewById(R.id.log_on)
        logOn.isEnabled = false

//        将EditText的文本变化转化为flow流
        val accountFlow = viewTextChangeFlow(account)
        val passwordFlow = viewTextChangeFlow(password)

//        开启协程
        lifecycleScope.launchWhenStarted {
//        使用combine将事件合并 两个EditText的输入-->button是否可点
            accountFlow.combine(passwordFlow) { _, _ ->
                account.text.isNotEmpty() && password.text.isNotEmpty()
            }.collect {
                logOn.isEnabled = it
            }

            viewClickFlow(logOn)
                .sample(1000L)
                .collect {
                    Log.e(TAG,"正在登录...")
                }
        }
    }

4.2网络联合请求&本地缓存

模拟联合请求场景

  • 有一段文本的数据来源于两个接口数据的混合处理,并且要分别进行本地缓存

  • 数据优先从本地获取,如果获取不到,在进行网络请求,请求完成后,在进行本地缓存

  • 两个接口的数据要同时展示

需求分析

多级缓存,请求时先判断本地缓存是否有数据,如果没有数据,则进行网络请求

  • 将本地缓存的数据和网络请求的数据视为数据流,并按顺序发送数据流。并将第一个有效的数据返回

  • 使用组合操作符concat(merge) 将本地缓存数据流,网络请求的数据流合并为一个流,并串行依次执行

  • 使用过滤操作符take(1)获取合并流的第一个有效元素

联合请求:将多个网络请求的数据源,同时进行展示

  • 将两个数据内容视为数据流,并一对一进行合并为一个新的数据流

  • 使用组合操作符zip,可以满足需求

4.2.1 Rxjava实现

将网络请求和本地获取数据包装为Observable

object DataControl {
    const val BAI_DU = "https://www.baidu.com/"
    const val SOU_GOU = "https://www.sogou.com/"
    private const val SP_NAME = "sp_name"
    private val retrofit: RetrofitApi by lazy {
        Retrofit.Builder()
            .addConverterFactory(ScalarsConverterFactory.create())
            .client(OkHttpClient())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl(BAI_DU)
            .build()
            .create(RetrofitApi::class.java)
    }

    //使用retrofit 可以直接转化为Observable
    fun getNetworkBaiduString() = retrofit.baidu(BAI_DU)
    fun getNetworkSougouString() = retrofit.souGou(SOU_GOU)
    //获取本地数据,并转化为Observable
    fun getLocalString(cxt: Context, channel: String): Observable<String?> {
        val baiduString = cxt.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
                .getString(channel, "")
        //如果本地没有数据,直接使用empty通知完成
        return if (baiduString.isNullOrEmpty()) Observable.empty() else Observable.just(baiduString)
    }
    //将数据保存到本地
    fun setLocalString(cxt: Context, channel: String, data: String) {
        val editor = cxt.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE).edit()
        editor.putString(channel, data)
        editor.apply()
    }

    interface RetrofitApi {
        @GET
        fun baidu(@Url url: String?): Observable<String?>
        @GET
        fun souGou(@Url url: String?): Observable<String?>
    }
}

多级缓存的实现

private fun getBaiduData():Observable<String?> {
    val baiduLocalObservable = DataControl
        .getLocalString(this, DataControl.BAI_DU)
        .map {
            //进行额外处理
            "数据来源:本地 百度->$it"
        }

    val baiduNetworkObservable = DataControl
        .getNetworkBaiduString()
        .subscribeOn(Schedulers.io()) // 切换到IO线程进行网络请求
        .observeOn(AndroidSchedulers.mainThread()) // 切换回到主线程 处理请求结果
        .map {
            //保存数据
            DataControl.setLocalString(this, DataControl.BAI_DU, it)
            //进行额外处理
            "数据来源:网络 百度->$it"
        }

    //获取百度数据
    //concat 串行执行本地。网络的两个数据流
    //只返回第一个数据
   return Observable.concat(baiduLocalObservable, baiduNetworkObservable)
        .take(1)
}

private fun getSouGouData():Observable<String?> {
    val souGouLocalObservable = DataControl
        .getLocalString(this, DataControl.SOU_GOU)
        .map {
            "数据来源:本地 搜狗->$it"
        }

    val souGouNetworkObservable = DataControl
        .getNetworkSougouString()
        .subscribeOn(Schedulers.io()) // 切换到IO线程进行网络请求
        .observeOn(AndroidSchedulers.mainThread()) // 切换回到主线程 处理请求结果
        .map {
            DataControl.setLocalString(this, DataControl.SOU_GOU, it)
            "数据来源:网络 搜狗->$it"
        }

    //获取百度数据
    //concat 串行执行本地。网络的两个数据流
    //只返回第一个数据
    return Observable.concat(souGouLocalObservable, souGouNetworkObservable)
        .take(1)
}

联合请求的实现

val baidu: TextView = findViewById(R.id.baidu)
val souGou: TextView = findViewById(R.id.souGou)
val button :Button = findViewById(R.id.button)
button.setOnClickListener {
    //zip,将百度和搜狗的数据一一对应进行组合。然后发出去
    Observable.zip(getBaiduData(),getSouGouData()) { s1, s2 ->
        Pair(s1, s2)
    }.subscribe {
        baidu.text = "时间: ${System.currentTimeMillis()} ${it.first}"
        souGou.text ="时间: ${System.currentTimeMillis()} ${it.second}"
    }
}

实现效果:

此处应该有视频,但掘金不支持视频

4.2.2 flow实现

将网络请求和本地获取数据包装为flow

object DataFlowControl {
    const val BAI_DU = "https://www.baidu.com/"
    const val SOU_GOU = "https://www.sogou.com/"
    private const val SP_NAME = "sp_name"
    private val retrofit: RetrofitApi by lazy {
        Retrofit.Builder()
            .addConverterFactory(ScalarsConverterFactory.create())
            .client(OkHttpClient())
            .addCallAdapterFactory(FlowCallAdapterFactory.create())
            .baseUrl(BAI_DU)
            .build()
            .create(RetrofitApi::class.java)
    }

    //使用retrofit 可以直接转化为Observable
    fun getNetworkBaiduString() = retrofit.baidu(BAI_DU)
    fun getNetworkSouGouString() = retrofit.souGou(SOU_GOU)

    //获取本地数据,并转化为Observable
    fun getLocalString(cxt: Context, channel: String): Flow<String?> {
        val baiduString = cxt.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
                .getString(channel, "")
        //如果本地没有数据,直接使用empty通知完成
        return if (baiduString.isNullOrEmpty()) emptyFlow() else flowOf(baiduString)
    }
    //将数据保存到本地
    fun setLocalString(cxt: Context, channel: String, data: String) {
        val editor = cxt.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE).edit()
        editor.putString(channel, data)
        editor.apply()
    }

    interface RetrofitApi {
        @GET
        fun baidu(@Url url: String?): Flow<String?>
        @GET
        fun souGou(@Url url: String?): Flow<String?>
    }
}

多级缓存的实现

private fun getBaiduFlowData():Flow<String?>{
    val baiduLocalFlow = DataFlowControl
        .getLocalString(this, DataControl.BAI_DU)
        .map {
            //进行额外处理
            "数据来源:本地 百度->$it"
        }

    val baiduNetWorkFlow = DataFlowControl
        .getNetworkBaiduString()
        .flowOn(Dispatchers.IO)
        .map {
            //保存数据
            it?.let { DataControl.setLocalString(this, DataControl.BAI_DU, it) }
            //进行额外处理
            "数据来源:网络 百度->$it"
        }.flowOn(Dispatchers.Main)

    //获取百度数据
    //merge 串行执行本地。网络的两个数据流
    //take(1)只返回第一个数据
    return listOf(baiduLocalFlow, baiduNetWorkFlow)
        .merge()
        .take(1)

}


private fun getSouGouFlowData():Flow<String?>{
    val souGouLocalFlow = DataFlowControl
        .getLocalString(this, DataControl.SOU_GOU)
        .map {
            //进行额外处理
            "数据来源:本地 搜狗->$it"
        }

    val souGouNetWorkFlow = DataFlowControl
        .getNetworkSouGouString()
        .flowOn(Dispatchers.IO)
        .map {
            //保存数据
            it?.let {DataControl.setLocalString(this, DataControl.SOU_GOU, it)}
            //进行额外处理
            "数据来源:网络 搜狗->$it"
        }.flowOn(Dispatchers.Main)
    //获取搜狗数据
    //merge 串行执行本地。网络的两个数据流
    //take(1)只返回第一个数据
    return listOf(souGouLocalFlow, souGouNetWorkFlow)
        .merge()
        .take(1)
}

联合请求的实现

private fun requestFlow() {
    val baidu: TextView = findViewById(R.id.baidu)
    val souGou: TextView = findViewById(R.id.souGou)
    val button: Button = findViewById(R.id.button)
    button.setOnClickListener {
        lifecycleScope.launch {
            //zip,将百度和搜狗的数据一一对应进行组合。然后发出去
            getBaiduFlowData().zip(getSouGouFlowData()) { s1, s2 ->
                Pair(s1, s2)
            }.collect {
                baidu.text = "时间: ${System.currentTimeMillis()} ${it.first}"
                souGou.text = "时间: ${System.currentTimeMillis()} ${it.second}"
            }
        }
    }
}

实现效果如上

5.参考文档:

Kotlin Flow 介绍
异步流 kotlin语言中文站

对比 RxJava 入门 Kotlin-flow - 掘金

【Kotlin Flow】 一眼看全——Flow操作符大全 - 掘金

Carson带你学Android:这是一篇清晰易懂的Rxjava入门教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/142600.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

量化策略——准备2 量化技能树量化术语

文章目录量化技能树量化/金融术语1. 俗语2. 持仓术语3. 资金术语4. 策略术语5. 股票软件界面实用术语量化必然用到的核心价格数据其他数据/指标含义6. 委托单术语量化技能树 首先&#xff0c;量化金融&#xff08;Quantitative Finance&#xff0c;简称“量化”&#xff0c;Qu…

《小猫猫大课堂》三轮1——深度解析数据在内存中的存储

宝子&#xff0c;你不点个赞吗&#xff1f;不评个论吗&#xff1f;不收个藏吗&#xff1f; 最后的最后&#xff0c;关注我&#xff0c;关注我&#xff0c;关注我&#xff0c;你会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的很重…

学习网络协议

概述 先从知乎盗个图&#xff1a;怎么开始学习网络协议&#xff1f; - 知乎 1、应用层 作用&#xff1a;定义数据格式并按照对应的格式解读数据。 2、传输层 作用&#xff1a;定义端口&#xff0c;标识应用程序身份&#xff0c;实现端口到端口的通信。 3、网络层 作用&…

5个技巧提高项目领导技能

作为项目经理&#xff0c;一个典型的工作日涉及处理许多任务。监督不同的时间表和里程碑。解决问题&#xff0c;主动解决瓶颈。 这些后勤工作很重要&#xff0c;但您也知道成功的项目管理比协调这些细节要多得多。为了做好你的工作&#xff08;并且把它做好&#xff09;&am…

C语言模拟QT的信号与槽功能

文章目录前言一、Qt信号与槽的实现机理二、简化后的实现步骤1. 定义一些必要的宏2. 实现声明信号的宏3. 实现发射信号的宏4. 取代QObject类5. 实现connect函数6. 可有可无的slots三、完整的代码实现四、使用方法与QT中的区别1. SIG_SLOT_OBJ取代QObject2. 定义信号不同3. 发射信…

【NI Multisim 14.0原理图环境设置——原理图的组成】

目录 序言 一、原理图的组成 &#x1f46c; 1. 元器件 &#x1f46c;2. 仪表 &#x1f46c;3.导线 &#x1f46c;4.丝印层 &#x1f46c;5. 端口 &#x1f46c;6.网络标号 &#x1f46c;7.电源符号 序言 NI Multisim最突出的特点之一就是用户界面友好。它可以使电路设…

为iframe正名,你可能并不需要微前端

作者&#xff1a;刘显安(码怪) 任何新技术、新产品都是有一定适用场景的&#xff0c;它可能在当下很流行&#xff0c;但它不一定在任何时候都是最优解。 前言 最近几年微前端很火&#xff0c;火到有时候项目里面用到了iframe还要偷偷摸摸地藏起来生怕被别人知道了&#xff0c;…

Linux学习笔记——Tomcat安装部署

5.2、Tomcat安装部署 5.2.1、简介 Tomcat是由Apache开发的一个Servlet容器&#xff0c;实现了对Servlet和JSP的支持&#xff0c;并提供了作为Web服务器的一些特有功能&#xff0c;如Tomcat管理和控制平台、安全域管理和Tomcat阀等。 简单来说&#xff0c;Tomcat是一个WEB应用…

内核解读之内存管理(3)内存管理三级架构之内存区域zone

文章目录1、zone类型2、zone结构体3、zone的初始化流程1、zone类型 NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快, 而Linux为了兼容NUMA结构, 把物理内存…

Flink数据流类型之间的转换(WindowedStream、DataStream、KeyedStream、AllWindowStream之间的转换)

Flink提供了一些流API&#xff0c;其中包括WindowedStream、DataStream、KeyedStream和AllWindowStream。 &#x1f34a;WindowedStream是一种特殊的流&#xff0c;其中数据已按时间或数据元素的键进行分组&#xff0c;并且每个分组的数据都在窗口中按时间划分。这意味着&…

2023年出入境政策-喜忧参半

2023年已经到来&#xff0c;随着卫健委公布中国防控新冠措施调整优化以后&#xff0c;出入境政策相应也有了很大变化&#xff0c;知识人网小编概括为喜忧参半。喜的是从国外入境中国不再需要集中隔离&#xff1b;忧的是有些国家对于中国人入境增加了核酸检测要求。下面我们就这…

第一章 Java入门开发

第一章 Java入门开发 目录一&#xff0e; 概述二&#xff0e; JDK1. 概述2. 安装3. JDK目录一&#xff0e; 概述 Java是一门高级程序设计语言&#xff0c;是支持跨平台和完成面向对象的程序设计语言。针对不同的开发市场&#xff0c;sun公司将Java分为Java SE&#xff08;标准版…

关于clip通信架构设计的调研

网络上大部分关于clip-as-service的描述都是关于它如何使用&#xff0c;基于它的编码功能上去计算文本相似度&#xff0c;根据文字推荐图片等等&#xff0c;只有作者的创作思路里面提及通信架构的设计。 作者博客&#xff1a; 链接: link 如何解决多个客户端同时请求服务端的场…

STS4中MVC项目中把log4j从1.x升级到2.x中遇到的两个问题

文章目录问题一 升级后看Maven Dependencies中还是有依赖1.x的log4j问题二 web.xml配置不对项目原来的log4j版本是1.2.14&#xff0c;有漏洞需要升级到2.18.0.问题一 升级后看Maven Dependencies中还是有依赖1.x的log4j 原因是有关联依赖&#xff0c; 项目中别的jar库有依赖低…

【算法笔记】【专题】RMQ 问题:ST表/树状数组/线段树

0. 前言 好久没更算法笔记专栏了&#xff0c;正好学了新算法来更新…… 这也是本专栏的第一个专题问题&#xff0c;涉及到三种数据结构&#xff0c;如果写得有问题请各位大佬多多指教&#xff0c;谢谢&#xff01; 1. 关于 RMQ 问题 RMQ 的全称是 Range Minimum/Maximum Que…

《Linux运维实战:Centos7.6基于docker-compose一键离线部署单节点redis6.2.8 》

一、部署背景 由于业务系统的特殊性&#xff0c;我们需要面向不通的客户安装我们的业务系统&#xff0c;而作为基础组件中的redis针对不同的客户环境需要多次部署&#xff0c;作为一个运维工程师&#xff0c;提升工作效率也是工作中的重要一环。所以我觉得有必要针对redis6.2.8…

使用 .NET 标记游戏地图关键坐标点

本文以天涯明月刀 OL 游戏的云上之城探索玩法为例&#xff0c;介绍如何使用 .NET 在游戏地图中标记大量关键坐标点。 1. 背景 大概很多程序员都是喜欢玩游戏的吧&#xff0c;我也不例外。我们经常会看到电视剧中的各路游戏大神&#xff0c;要么是有只有他一个人会的骚操作&…

Linux--信号--信号的产生方式--核心转储--0104

1. 什么是信号 生活中的信号&#xff1a;红绿灯&#xff0c;狼烟&#xff0c;撤退、集合...。 我们认识这些信号&#xff0c;首先是因为自己记住了对应场景下的信号后续需要执行的动作。如果信号没有产生&#xff0c;我们依旧知道如何处理这个信号。收到信号&#xff0c;我们…

springboot学习(七十八) springboot中通过自定义注解实现数据脱敏的功能

文章目录前言一、引入hutools工具类二、定义常用需要脱敏的数据类型的枚举三、定义脱敏方式枚举四、自定义脱敏的注解五、自定义Jackson的序列化方式六、使用七、脱敏效果前言 对于某些接口返回的信息&#xff0c;涉及到敏感数据的必须进行脱敏操作&#xff0c;例如银行卡号、…

带你了解ssh服务过程

远程连接服务 1、什么是远程连接服务器 远程连接服务器通过文字或图形接口方式来远程登录系统&#xff0c;让你在远程终端前登录linux主机以取得可操作主机接口&#xff08;shell&#xff09;&#xff0c;而登录后的操作感觉就像是坐在系统前面一样。 2、远程连接服务器的功…