How to Observe EditText Changes in Background

In this post I’ll show some useful tips for observing View changes in the background using RxAndroid and RxBinding.

Let’s start with an easy example of observing EditText changes.

Subscription subscription = RxTextView.textChanges(editText)
    .subscribeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<CharSequence>() {
      @Override public void onCompleted() { }
      @Override public void onError(Throwable e) { }
      @Override public void onNext(CharSequence charSequence) { }
    });

This example will subscribe and observe EditText changes on the main thread. Say we want now to filter the CharSequence from the current EditText and proceed to the onNext() method only if the CharSequence is not empty? How do we achieve that in the Observable chain? We can add a function filter():

.filter(new Func1<CharSequence, Boolean>() {
  @Override public Boolean call(CharSequence charSequence) {
    return charSequence.length() != 0;
  }
})

In this case function filter() doesn’t add any problems in terms of freezing the UI because it’s super fast to process. But! Say we want to have a really heavy CharSequence processing that probably will freeze the UI for a second.

.filter(new Func1<CharSequence, Boolean>() {
  @Override public Boolean call(CharSequence charSequence) {
    SystemClock.sleep(1000); // Simulate the heavy stuff.
    return charSequence.length() != 0;
  }
})

We don’t want the UI thread to hang and prevent the user from typing the text, so we want to do the filter thing on another thread. In order to achieve that, we can just add a line with observeOn() before the filter function.

.observeOn(Schedulers.computation())

In this case, we filter the CharSequence on the computation thread, however now our Observer<CharSequence> will receive the result on the computation thread, too! So if we want to update some views in the onNext() method, we need to add a line after the filtering function.

.observeOn(AndroidSchedulers.mainThread())

Everything seems to be OK by now but there are some issues which might occur while using such approach:

  1. rx.exceptions.MissingBackpressureException will likely occur when the user will be either typing too fast or deleting the symbols too fast.
  2. Memory leaking if we don’t unsubscribe from the Subscription. Moreover, if some Views were referenced in the onNext() method, then there’s a potential NullPointerException.

To address the issue #1, we can use the debounce() function. It is very useful for obtaining only those items that are not followed by another item within a specified duration1.

We should add the debounce() function before any filtering.

.debounce(500, TimeUnit.MILLISECONDS)

To address the issue #2, we must unsubscribe from the Observable<CharSequence>. If we do that, we will be on the safe side.

@Override protected void onStop() {
  super.onStop();

  if (!subscription.isUnsubscribed()) {
    subscription.unsubscribe();
  }
}

The resulting rx chain looks like this:

subscription = RxTextView.textChanges(editText)
    .debounce(500, TimeUnit.MILLISECONDS)
    .observeOn(Schedulers.computation())
    .filter(new Func1<CharSequence, Boolean>() {
      @Override public Boolean call(CharSequence charSequence) {
        SystemClock.sleep(1000); // Simulate the heavy stuff.
        return charSequence.length() != 0;
      }
    })
    .subscribeOn(AndroidSchedulers.mainThread())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<CharSequence>() {
      @Override public void onCompleted() { }
      @Override public void onError(Throwable e) {
        Log.d("RxTesting", "Error.", e);
      }
      @Override public void onNext(CharSequence charSequence) {
        content.setText(charSequence);
        count.setText(String.valueOf(charSequence.length()));
      }
    });

You can find a sample application here.

  1. The debounce operator emits only those items from the source Observable that are not followed by another item within a specified duration.

comments powered by Disqus