RxJava 沉思录(三):时间维度(1)

Observable debounced = likeAction.debounce(1000, TimeUnit.MILLISECONDS);
debounced.zipWith(
debounced.startWith(like),
(last, current) -> last == current ? new Pair<>(false, false) : new Pair<>(true, current)
)
.flatMap(pair -> pair.first ? Observable.just(pair.second) : Observable.empty())
.subscribe(like -> {
if (like) {
sendCancelLikeRequest(postId);
} else {
sendLikeRequest(postId);
}
});

zipWith 操作符可以把两个 Observable 发射的相同序号(同为第 x 个)的元素,进行运算转换,得到的新元素作为新的 Observable 对应序号所发射的元素。参考资料:ZipWith

上面的代码,我们可以看到,首先我们对事件流做了一次 debounce 操作,得到 debounced 事件流,然后我们把 debounced 事件流和 debounced.startWith(like) 事件流做了一次 zipWith 操作。相当于新的这个 Observable 中发射的第 n 个元素(n >= 2)是由 debounced 事件流中的第 n 和 第 n-1 个元素运算得到的(新的这个 Observable 中发射的第 1 个元素是由 debounced 事件流中的第 1 个元素和原始点赞状态 like 运算而来)。

运算的结果是得到一个 Pair 对象,它是一个双布尔类型二元组,二元组第一个元素为 true 代表这个事件不该被忽略,应该被观察者观察到;若为 false 则应该被忽略。二元组的第二个元素仅在第一个元素为 true 的情况下才有意义,true 表示应该发起一次点赞操作,而 false 表示应该发起一次取消点赞操作。上面提到的“运算”具体运算的规则是,比较两个元素,若相等,则把二元组的第一个元素置为 false,若不相等,则把二元组的第一个元素置为 true, 同时把二元组的第二个元素置为 debounced 事件流发射的那个元素。

随后的 flatMap 操作符完成了两个逻辑,一是过滤掉二元组第一个元素为 false 的二元组,二是把二元组转化回最初的 Boolean 事件流。其实这个逻辑也可由 filtermap 两个操作符配合完成,这里为了简单用了一个操作符。

虽然上面用了不少篇幅解释了每个操作符的意义,但其实核心思想是简单的,就是在原先 debounce 操作符的基础上,把得到的事件流里每个元素和它的上一个元素做比较,如果这个元素和上个元素相同(例如在已赞状态下再次发起点赞操作), 就把这个元素过滤掉,这样最终的观察者里只会在在真正需要改变点赞状态的时候才会发起网络请求了。

我们考虑用 Callback 实现相同逻辑,虽然比较本次操作与上次操作这样的逻辑通过 Callback 也可以做到,但是 debounce 这个操作符完成的任务,如果要使用 Callback 来实现就非常复杂了,我们需要定义一个计时器,还要负责启动与关闭这个计时器,我们的 Callback 内部会掺杂进很多和观察者本身无关的逻辑,相比 RxJava 版本的纯粹相去甚远。

检测双击事件

首先,我们需要定义双击事件,不妨先规定两次点击小于 500 毫秒则为一次双击事件。我们先使用 Callback 的方式实现:

long lastClickTimeStamp;

btn.setOnClickListener(v -> {
if (System.currentTimeMillis() - lastClickTimeStamp < 500) {
// handle double click
}
});

上面的代码很容易理解,我们引入一个中间变量 lastClickTimeStamp, 通过比较点击事件发生时和上一次点击事件的时间差是否小于 500 毫秒,来确认是否发生了一次双击事件。那么如何通过 RxJava 来实现呢?就和上一个例子一样,我们可以在时间维度对 Observable 发射的事件进行重新组织,只过滤出与上次点击事件间隔小于 500 毫秒的点击事件,代码如下:

Observable clicks = RxView.clicks(btn)
.map(o -> System.currentTimeMillis())
.share();

clicks.zipWith(clicks.skip(1), (t1, t2) -> t2 - t1)
.filter(interval -> interval < 500)
.subscribe(o -> {
// handle double click
});

我们再一次用到了 zipWith 操作符来对事件流自身相邻的两个元素做比较,另外这次代码中使用了 share 操作符,用来保证点击事件的 Observable 被转为 Hot Observable

RxJava中,Observable可以被分为Hot ObservableCold Observable,引用《Learning Reactive Programming with Java 8》中一个形象的比喻(翻译后的意思):我们可以这样认为,Cold Observable在每次被订阅的时候为每一个Subscriber单独发送可供使用的所有元素,而Hot Observable始终处于运行状态当中,在它运行的过程中,向它的订阅者发射元素(发送广播、事件),我们可以把Hot Observable比喻成一个电台,听众从某个时刻收听这个电台开始就可以听到此时播放的节目以及之后的节目,但是无法听到电台此前播放的节目,而Cold Observable就像音乐 CD ,人们购买 CD 的时间可能前后有差距,但是收听 CD 时都是从第一个曲目开始播放的。也就是说同一张 CD ,每个人收听到的内容都是一样的, 无论收听时间早或晚。

仅仅是上面这个双击检测的例子,还不能体现 RxJava 的优越性,我们把需求改得更复杂一点:如果用户在“短时间”内连续多次点击,只能算一次双击操作。这个需求是合理的,因为如果按照上面 Callback 的写法,虽然可以检测出双击操作,但是如果用户快速点击 n 次(间隔均小于 500 毫秒,n >= 2), 就会触发 n - 1 次双击事件,假设双击处理函数里需要发起网络请求,会对服务器造成压力。要实现这个需求其实也简单,和上一个例子类似,我们用到了 debounce 操作符:

Observable clicks = RxView.clicks(btn).share()

clicks.buffer(clicks.debounce(500, TimeUnit.MILLISECONDS))
.filter(events -> events.size >= 2)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> {
// handle double click
});

buffer 操作符接受一个 Observable 为参数,这个 Observable 所发射的元素是什么不重要,重要的是这些元素发射的时间点,这些时间点会在时间维度上把原来那个 Observable 所发射的元素划分为一系列元素的组,buffer 操作符返回的新的 Observable 发射的元素即为那些“组”。
参考资料: Buffer

上面的代码通过 bufferdebounce 两个操作符很巧妙的把点击事件流转化为了我们关心的 “短时间内点击次数超过 2 次” 的事件流,而且新的事件流中任意两个相邻事件间隔必定大于 500 毫秒。

在这个例子中,如果我们想要使用 Callback 去实现相似逻辑,代码量肯定是巨大的,而且鲁棒性也无法保证。

搜索提示

我们平时使用的搜索框中,常常是当用户输入一部分内容后,下方就会显示对应的搜索提示,以支付宝为例,当在搜索框输入“蚂蚁”关键词后,下方自动刷新和关键词相关的结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了简化这个例子,我们不妨定义根据关键词搜索的接口如下:

public interface Api {
@GET(“path/to/api”)
Observable<List> queryKeyword(String keyword);
}

查询接口现在已经确定下来,我们考虑一下在实现这个需求的过程中需要考虑哪些因素:

  1. 防止用户输入过快,触发过多网络请求,需要对输入事件做一下防抖动。
  2. 用户在输入关键词过程中可能触发多次请求,那么,如果后一次请求的结果先返回,前一次请求的结果后返回,这种情况应该保证界面展示的是后一次请求的结果。
  3. 用户在输入关键词过程中可能触发多次请求,那么,如果后一次请求的结果返回时,前一次请求的结果尚未返回的情况下,就应该取消前一次请求。

综合考虑上面的因素以后,我们使用 RxJava 实现的对应的代码如下:

RxTextView.textChanges(input)
.debounce(300, TimeUnit.MILLISECONDS)
.switchMap(text -> api.queryKeyword(text.toString()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(results -> {
// handle results
});

switchMap 这个操作符与 flatMap 操作符类似,但是区别是如果原 Observable 中的两个元素,通过 switchMap 操作符都转为 Observable 之后,如果后一个元素对应的 Observable 发射元素时,前一个元素对应的 Observable 尚未发射完所有元素,那么前一个元素对应的 Observable 会被自动取消订阅,尚未发射完的元素也不会体现在 switchMap 操作符调用后产生的新的 Observable 发射的元素中。 参考资料:SwitchMap
我们分析上面的代码,可以发现: debounce 操作符解决了问题 1switchMap 操作符解决了问题 23。这个例子可以很好的说明,RxJava 的 Observable 可以通过一系列操作符从时间的维度上重新组织事件,从而简化观察者的逻辑。这个例子如果使用 Callback 来实现,肯定是十分复杂的,需要设置计时器以及一堆中间变量,观察者中也会掺杂进很多额外的逻辑,用来保证事件与事件的依赖关系。
(未完待续)
本文属于 “RxJava 沉思录” 系列,欢迎阅读本系列的其他分享:


如果您对我的技术分享感兴趣,欢迎关注我的个人公众号:麻瓜日记,不定期更新原创技术分享,谢谢!😃
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了5、6年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

w-1711900382090)]

[外链图片转存中…(img-VdFNxpId-1711900382090)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值