Maxiee 的 RxJava 学习指南 (4) - RxBinding

2017-06-21

介绍

从这一篇我们开始对 RxBinding 库的学习, 主要学习它对按钮的封装, 以及它内部的实现原理.

对于RxBinding 库的使用, 网上已经有许多优秀的教程了. 我简单地梳理了一下:

文章介绍
使用RxBinding响应控件的异步事件以一个 Demo 示例来讲解, RxToolbar, RxSnackbar
RxBinding 学习笔记基本用法, 点击, ListView 点击事件, 结合使用操作符
RxBinding(JakeWharton/RxBinding)实例: 验证过滤, 流量控制
RxJava RxBinding基础按钮点击, 按钮防抖, EditText 文本改变, CheckBox, RecyclerView
一些RxBinding使用场景按钮防抖, 多次监听, 倒计时, 表单验证
RxJava 和 RxAndroid 四(RxBinding的使用)按钮防抖, 按钮的长按时间监听, ListView, CheckBox

实例项目

我在 GitHub 上建立了一个项目 MaxieeRxLearning 作为对学习的总结.

本文中我会用 MaxieeRxLearning 中的代码来进行讲解, 因此建议先将这个项目 Clone 下来参照着学习.

RxBinding 按钮的演示代码位于 ButtonFragment, 截图如下:

一共有 4 种常用情况, 下面依次说明.

普通点击

代码:

RxView.clicks(mNormalClickButton).subscribe(
        Object -> mNormalClickTextView.setText(
                "Clicked at " + System.currentTimeMillis()));

每次点击按钮, 订阅者就会被触发, 执行操作. 在这里所执行的操作是更新 TextView.

长点击

代码:

RxView.longClicks(mLongClickButton).subscribe(
        object -> mLongClickTextView.setText(
                "Long click at " + System.currentTimeMillis()));

只有长点击才会响应, 普通点击不会触发.

防抖点击

代码:

RxView.clicks(mThrottleClickButton)
        .throttleFirst(5, TimeUnit.SECONDS)
        .subscribe(
                object -> mThrottleTextView.setText(
                        "Clicked at " + System.currentTimeMillis()));

上面代码中防抖点击的功能时这样的:

  • 第一次的点击能够触发观察者
  • 在这之后的 5 秒内点击都不会触发
  • 过了 5 秒后复位, 再次点击回到第一步情况

注意, 防抖的功能是将 RxBinding 的 RxView.clicks() 和 RxJava 的 throttleFirst 操作符组合起来使用的. throttleFirst 的文档在这里.

Enable 控制

这个 Demo 是有一个 CheckBox 它能控制 Button 的 Enable 属性, CheckBox 勾选 Button 的 enable 为 true, 按钮可点击, 反之亦然. 代码:

RxCompoundButton.checkedChanges(mCheckEnable).subscribe(
        RxView.enabled(mCheckButton));

RxView.clicks(mCheckButton).subscribe(
        object -> mCheckTextView.setText(
                "Clicked at " + System.currentTimeMillis()));

其中:

  • RxCompoundButton.checkedChanges 观察的是 CheckBox 的勾选状态, 在观察者中调用 RxView.enabled 改变按钮的 Enable 属性.
  • 这里细心的同学会发现, enabled 传入参数呢? enabled 类型是 Consumer<? super Boolean>, checkedChanges 类型是 InitialValueObservable<Boolean>, 都被封装了.
  • 按钮点击的代码跟普通点击是一样的

这个 Demo 一般应用于点击 "已阅读协议" 然后才允许用户注册的页面.

RxView.clicks 实现

前面介绍了很多实用的例子, 它们的底层是怎么实现的呢? 我们以 RxView.clicks 为例来看一下.

给按钮设置回调的方法是:

view.setOnClickListener(OnClickListener listener);

我们要做的是, 封装一个 Observable, 当点击回调触发时, 向 observer 发一个 onNext, 那怎么实现呢?

首先我们创建一个 Observable:

final class MaxieeViewClickObservable extends Observable<Object> {
    private final View mView;

    ViewClickObservable(View view) {
        mView = view;
    }

    @Override
    protected void subscribeActual(Observer<? super Object> observer) { ... }
}

其中, subscribeActual 是干什么的呢? 它是 RxJava 内部的一个核心要点.

我们知道, 被观察者被观察者订阅, 他俩就建立起关系来了, 当被观察者发出数据就能触发观察者.

现在的问题是, 这个触发是怎么进行的? 就是通过 Observable 的 subscribeActual 实现的.

我们补全上面空着的 subscribeActual:

private static class MaxieeViewClickObservable extends Observable<Object> {
    private View mView;

    MaxieeViewClickObservable(View view) {
        mView = view;
    }

    @Override
    protected void subscribeActual(Observer<? super Object> observer) {
        mView.setOnClickListener(v -> observer.onNext(null));
    }
}

其中, 建立关系的方式很简单, 创建一个点击回调绑定给 view, 回调的内容是调用 observer 的 onNext.

RxJava 中被观察者和观察者的触发就是通过这种机制建立起来的.

RxBinding 提供了一个工具方法, 也就是 RxView.clicks() 做了一个封装, 它的实现是这样:

public static Observable<Object> clicks(View view) {
    return new MaxieeViewClickObservable(view);
}

这样我们的 clicks 跟 RxView.clicks() 就能一样使用了:

clicks(mHomeBrewButton).subscribe(
        object -> mHomeBrewTextView.setText(
                "Clicked at " + System.currentTimeMillis()));

上面这个代码展示了 RxView.clicks 的最核心的实现原理.

在理解了核心原理之后, 这段代码还有不完善的地方, 许多地方还有问题, 我们将在下一节中来完善它们.

RxView.clicks 进阶

上面我们实现的代码, 第一个问题是在解绑时会有问题.

我们用以下代码来解绑:

Disposable d = clicks(mHomeBrewButton).subscribe(
        object -> mHomeBrewTextView.setText(
                "Clicked at " + System.currentTimeMillis()));

d.dispose();

这样再次运行, 点击按钮确实不会触发 TextView 变化了. 表面上没问题, 实际上有问题:

dispose 之后, button 的 OnClickListener 实现中仍然持有者 observer 对象. 这回造成内存泄漏.

怎么解决呢? 在 subscribeActual 中调用 observer 的 onSubscribe 方法, 传入一个 Disposable, 在 observer dispose 的时候会调用这个 Disposable 的 onDispose, 在里面将点击回调清空.

其中 onSubscribe 是 RxJava 中观察者 dispose 取消订阅时, 被观察者进行资源释放的机制.

写成代码就是:

@Override
protected void subscribeActual(Observer<? super Object> observer) {
    mView.setOnClickListener(v -> observer.onNext(null));

    observer.onSubscribe(new Disposable() {
        private boolean mDisposed = false;

        @Override
        public void dispose() {
            Log.d("maxiee", "clear the click listener of button");
            mView.setOnClickListener(null);
            mDisposed = true;
        }

        @Override
        public boolean isDisposed() {
            return mDisposed;
        }
    });
}

这样, 当观察者取消订阅时, 按钮的点击回调也就清空了, 也就是释放了资源.

有一点要注意的是, 我在上面的实现跟 RxBinding 的实现有所不同, 但是原理是一样的, 上面这样写比较易于理解.

RxBinding 的实现如下:

final class ViewClickObservable extends Observable<Object> {
  private final View view;

  ViewClickObservable(View view) {
    this.view = view;
  }

  @Override protected void subscribeActual(Observer<? super Object> observer) {
    if (!checkMainThread(observer)) {
      return;
    }
    Listener listener = new Listener(view, observer);
    observer.onSubscribe(listener);
    view.setOnClickListener(listener);
  }

  static final class Listener extends MainThreadDisposable implements OnClickListener {
    private final View view;
    private final Observer<? super Object> observer;

    Listener(View view, Observer<? super Object> observer) {
      this.view = view;
      this.observer = observer;
    }

    @Override public void onClick(View v) {
      if (!isDisposed()) {
        observer.onNext(Notification.INSTANCE);
      }
    }

    @Override protected void onDispose() {
      view.setOnClickListener(null);
    }
  }
}

其中:

  • OnClickListener 跟 MainThreadDisposable 合并成一个 Listener
  • MainThreadDisposable 是 RxAndroid 提供的一种 Disposable, 等我们学习 RxAndroid 时再回头看

总结

在本篇中, 我们首先学习了 RxBinding 按钮的常用使用场景. 在此基础之上, 我们深入 RxBinding 底层, 学习了它的实现原理. 在学习原理的过程中, 我们学习了 RxJava2 的绑定与解绑的底层实现机制.

原理学会之后, 会发现各种绑定都很简单, 因此在下一篇中我会加快一点速度, 罗列 RxBinding 控件的使用场景.