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) 按顺序接收事件 & 作出对应的响应动作。
特点:
-
操作符丰富
-
支持背压
-
支持线程切换操作
-
学习成本和上手难度较高
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)
执行结果
原理
这一块源码的实现,感兴趣可以看我之前的博客: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)
}
执行结果
原理
看了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") }
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")
}
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)
}
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
定义
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 ");
}
})
注意点:
-
如果未设置request,则默认未接收
-
观察者不接收事件的情况下,被观察者继续发送事件 & 存放到缓存区,再按需求取出
-
超出缓存区的数据,会按照策略进行处理
requested()
异步订阅下,**requested()**反向控制Observable
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 ");
}
})
背压策略
目前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策略,此时当缓冲区超出处理速度时,就会丢弃老数据
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入门教程