Maxiee 的 RxJava 学习指南 (3) - UI 交互

2017-06-13

介绍

在这一篇中, 我将继续对 RxJava-Android-Samples 项目的学习, 主要包含以下 3 个 Demo:

  • Accumulate Calls
  • Search Text Listener
  • Double Binding

这 3 个 Demo 都是与 UI 相关的, 包括监听控件的事件, 以及比较流行的 Data Binding. 通过学习, 我们将会学到如何将 RxJava 应用到界面开发中.

Accumulate Calls

这个 Demo 位于 BufferDemoFragment. 它的界面和说明如下:

截图说明
功能是统计 2s 内点击按钮的次数. 每次点击会立即展示一个 "GOT A TAP", 等 2s 结束后会显示者两秒内点击的次数.

下面来看具体的代码:

RxView.clicks(_tapBtn)
    .map(
        onClickEvent -> {
            _log("GOT A TAP");
            return 1;
        })
    .buffer(2, TimeUnit.SECONDS)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeWith(
        new DisposableObserver<List<Integer>>() {

            @Override
            public void onComplete() {}

            @Override
            public void onError(Throwable e) {}

            @Override
            public void onNext(List<Integer> integers) {
            if (integers.size() > 0) {
                _log(String.format("%d taps", integers.size()));
            } else {
                Timber.d("--------- No taps received ");
            }
        }
    });

从代码中可以看到, 在按钮上面套了一层 RxView.clicks. 这里用到了 jakewharton 大神的 RxBinding 库.

这个库我打算在下一篇博客中详细介绍, 在这里只需知道对 Button 使用 RxView.clicks(_tapBtn) 包一下就将 Button 的点击变成了一个被观察者.

除此之外, 在上面代码中主要使用了 buffer 操作符, 它起了关键地作用, 下面来详细看一下.

Buffer 操作符

其中的关键就在与 Buffer 这个操作符, 首先介绍一下它.

Buffer 操作符的官方文档在这里, 它的描述为:

周期性地收集被观察者发出的数据, 打成一个包 (一个列表), 将这个包发出去.

具体来说, 就是 Buffer 每隔一定周期收集源被观察者发出的数据, 将它打包成一个列表, 对外发出.

因此, 上述代码的大体流程, 使用 RxJava 的弹珠图来梳理为:

其中:

  • 多次点击按钮多发出多个 click 事件, 传入 buffer,
  • buffer(2, TimeUnit.SECONDS) 表示定时的周期为 2s
  • 一单时间到, buufer 将这段时间里收到的数据, 打包成一个列表传出去
  • onNext 的参数是 List<Integer> integers 它的长度表示这段时间内点击按钮的次数

Search Text Listener

这个 Demo 位于 DebounceSearchEmitterFragment. 它的界面跟说明如下:

截图说明
我们在输入框中输入文本, 当我们输入停下来的时候, 会自动在下方添加一条 "Searching for …" 的记录.

对应的实现代码为:

_disposable =
    RxTextView.textChangeEvents(_inputSearchText)
        .debounce(400, TimeUnit.MILLISECONDS) // default Scheduler is Computation
        .filter(changes -> isNotNullOrEmpty(changes.text().toString()))
        .observeOn(AndroidSchedulers.mainThread())
        .subscribeWith(_getSearchObserver());

其中, EditText 被套了一个 RxTextView.textChangeEvents, 这个还是 RxBinding 库 中的方法, 将 EditText 变成了一个被观察者, 每当文本变化, 会发出事件.

除此, 最重要的是用了一个 debounce 操作符, 下面来详细看一下.

Debounce 操作符

Debounce 的文档在这里, 它的描述为:

给定一个事件间隔, 对外发射在这段时间间隔里, 发出所收到数据的最后一个.

这里要注意的一点是:

假设被观察者已经发出一个数据传入 Debounce, 此时它会开始计时, 如果在计时时间结束之前又有一个数据传入 Debounce, 这时 Debounce 会重新开始计时

我们结合上面的代码来理解这段描述:

  • 首先, 每向 EditText 中打一个字符, 都会触发被观察者对外发射一个数据
  • 所发出的数据会传入 Debounce
  • 如果我们连续打字, 连续有数据传入 Debounce, 这时 Debounce 不断重置计时器
  • 一旦我们停止输入, 过了 400ms, Debounce 会对外输出最后一次的数据
  • 传给下一个 filter 操作符 (实现判空过滤功能)

这样, 我们通过 Debounce 操作符, 就为 EditText 提供了能够动态感知输入结束的能力.

下面我们再来看 filter 的作用.

Filter 操作符

Filter 的文档在这里, 它的描述为:

Filter 操作符接收一个方法, 这个方法是一个条件判断. 对于传入 Filter 的数据, 都传入这个方法里判断一下是否满足条件.

如果满足条件则对外发出, 如果不满足条件, 则不发出.

这样, 结合上面的代码不难看出, 将 Debounce 输出的数据传入 Filter, 目的是加入对 EditText 的判空检查.

如果 EditText 的内容是空的, Filter 就将它过滤掉, 不再对外发出了.

小结

在了解了 Debounce 和 Filter 两个操作符的功能后, 我们就完全弄明白了这个 Demo 中 EditText 够动态感知输入结束的原理了.

Double Binding

这个 Demo 位于 DoubleBindingTextViewFragment. 它的界面和截图如下:

截图说明
界面中有两个用于输入数字的 EditText, 当我们改变其中的一个的值, 下方的和就会自动变化.

这个功能是怎么实现的呢?

首先我们通过 ButterKnife 监听两个 EditText 的 OnTextChanged 事件:

@OnTextChanged({R.id.double_binding_num1, R.id.double_binding_num2})
public void onNumberChanged() {
    ...
}

这个是 ButterKnife 提供的特性, 跟 RxJava 无关, 跟 RxJava 有关的在方法的实现里, 它的实现为:

@OnTextChanged({R.id.double_binding_num1, R.id.double_binding_num2})
public void onNumberChanged() {
    float num1 = 0;
    float num2 = 0;

    if (!isEmpty(_number1.getText().toString())) {
        num1 = Float.parseFloat(_number1.getText().toString());
    }

    if (!isEmpty(_number2.getText().toString())) {
        num2 = Float.parseFloat(_number2.getText().toString());
    }

    _resultEmitterSubject.onNext(num1 + num2);
}

其中:

  • 当 EditText 内容变化, 就会调用 onNumberChanged
  • 我们拿出两个 EditText 的值
  • 对两个值相加, 作为数据传入 _resultEmitterSubject

现在的问题是 _resultEmitterSubject 是什么呢? 我们来看它的创建的代码:

_resultEmitterSubject = PublishProcessor.create();

从中可以看出, 它是一个 PublishProcessor.

可是问题又来了, PublishProcessor 是什么呢?

PublishProcessor

对这部分的讲解记录在 What's different in 2.0 的 Subjects and Processors 一节.

还记得第一篇中介绍过 ReactiveX 中的 Subject 的概念, 它在 RxJava 2.0 中细分为两种:

  • Subject 不支持背压
  • Process 支持背压

对于背压这个概念, 我还不太明白. 那么暂且在这里就把 PublishProcessor 当做 PublishSubject 来看待吧.

订阅

先梳理一下前面的流程, 如下图所示:

其中, 我们已经分析了 OnTextChanged, 调用 _resultEmitterSubjectonNext. 现在还剩下最后一步, 就是观察者订阅 _resultEmitterSubject 接收数据, 这部分的实现代码为:

_disposable =
    _resultEmitterSubject.subscribe(
        aFloat -> {
            _result.setText(String.valueOf(aFloat));
        });

这个很简单, 就是收到前面过程传入的合值, 在结果 TextView 中进行显示.

总结

至此, 我们就完成了对 3 个 Demo 的学习.

可以看出, 结合 RxJava 进行界面开发, 代码是比较优雅的, 尤其是在实现一些对控件的扩展操作, 例如 throttling 和监听输入.

在学习中我们还看到, jakewharton 大神的 RxBinding 库为开发提供了很多便利.

这个库的功能十分强大, 在下一篇的学习中, 我将对这个库进行详细地学习.