最近项目里有个点赞的功能需求,发现当用户对一条评论反复点赞的速度过快时,会引起轻微的数据错乱。对一条评论不停的点赞,评论数本应在+1
和-1
之间不断循环的,但是点赞/取消点赞的时候需要进行网络请求,将点击结果上传到服务器,所以点多了之后,由于延时的原因,会出现某些+1
或-1
操作会失效,具体表现为,对一个初始赞数为0
的评论进行这样一波疯狂操作后,点赞总数有可能变成负数,这无疑是不允许的。
至于要问有什么用户会这么无聊,跟打地鼠一样疯狂点赞,那当然非我们的测试小哥莫属了。
思考了各种解决方案的优劣后,最终采用了最省事的一个方法,给点赞按钮加个抖动阈值。正好最近写 RxJava 写得不亦乐乎,再加上不想为了这一处地方就引入 RxBinding 库,就想着自己写一下,于是顺手就写出了以下代码用以去除点击抖动:
mBinding.title.setOnClickListener(v -> {
Observable.create(e -> e.onNext(new Object()))
.throttleFirst(600, TimeUnit.MILLISECONDS)
.subscribe(o -> {
// 进行取消点赞或点赞的网络请求
});
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
自己也没测就直接交给测试小哥了,然后被暴击:“你特么怎么该都没改就给我测了,这表现和之前没区别啊!?”
于是我瞬间懵比,自己一测,确实没解决问题,在代码里面写了俩 Log 一瞧,这去抖动根本没起任何作用啊,然后又找了网上几篇直接用 RxJava 实现的点击去抖动功能,发现我这也没写错啊。
左思右想,最后去看了下 RxBinding 的源码,才发现问题的关键所在。去抖动,需要将点击事件这个“事件”转换成一个Observable
对象,而瞧我上面那段代码,直接在点击事件里生成的一个Observable
对象,导致的结果就是每点击一次,就产生了一个新的 RxJava 流,所以导致没有达到想要的“只取600ms内第一次点击”的效果。
思考了下,转换成如下写法,总算达成效果了。
Observable.create((ObservableOnSubscribe<View>) e ->
mBinding.title.setOnClickListener(v -> e.onNext(v))
)
.throttleFirst(600, TimeUnit.MILLISECONDS)
.subscribe(view -> {
// 进行取消点赞或点赞的网络请求
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
虽然 RxBinding 这个库也比较好用,但是我们的自定义控件好像就用不上它的,所以还是只能自己来实现去抖动了。
用 RxJava 实现的点击取抖动虽好,但有些情况还是顾不到,用它实现去抖动,就必须获取它的点击事件,将之转换成Observalbe
对象才行,但有些情况这样是行不通的,比如一个页面的批量点击功能的实现:
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.check_for_update_button:
break;
case R.id.clear_cache_button:
break;
case R.id.save_flow_toggle:
break;
case R.id.feedback_button:
break;
case R.id.logout_button:
break;
default:
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
在这个里面就根本没法获取到点击事件对象,所以要么将这些控件单独拿出来一个个实现,要么就用土办法,在上次点击后一定时间内让点击事件无效化,可以用如下代码:
private static final int CLICK_INTERVAL = 600;
private static long lastClickTime;
public static boolean clickValid() {
long currentClickTime = System.currentTimeMillis();
return currentClickTime - lastClickTime > CLICK_INTERVAL;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
如果只需要上述所有点击事件共用一个抖动时间,那么将上面这段代码写入统一的工具类,并在switch
语句之前调用即可,也可以个性化定制每个View
的抖动时间,那就写起来麻烦一点。在我看来是个“傻大黑粗”的方法。