In this article I want to show how RxJava and reactive programming in general can be used to simplify developing of UI components. As an example we are going to build a simple email registration form with text field for login and spinner to select domain of email.

user interface example screenshot


Also, you can download APK and open sample on device by clicking this link or watch video (300 kb) or gif (351 kb).

Editing login or selecting domain triggers update of text field with result email. Let's start with basic setup.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sample_email);
    mLoginText = (EditText) findViewById(R.id.login);
    mDomainSpinner = (Spinner) findViewById(R.id.domain);
    mResultText = (EditText) findViewById(R.id.result);
    mResultText.setEnabled(false);
    setupFields();
}

Instead of listening for text changes in listener we will process a sequence of text changes. Same applies to Spinner, we will work with a sequence of selected values.

RxAndroid provides class WidgetObservable, which has method text returning observable with changes of EditText.

UPDATE 03.12.2015:

Starting from RxAndroid 1.0.0 WidgetObservable is no longer present in library. It was converted to set of Rx(View, EditText, …) components which are provided by RxBinding library.

WidgetObservable.text(mLoginText, false),//false - do not emit start value

For Spinner we need to implement Observable by ourselves. One of the ways to implement it is to use PublishSubject<String> (items of our spinner are strings).

Quote from official documentation:

PublishSubject emits to an observer only those items that are emitted by the source Observable(s) subsequent to the time of the subscription.
public static Observable<String> observeSelect(Spinner spinner) {
      final PublishSubject<String> selectSubject = PublishSubject.create();
      // for production code, unsubscribe, UI thread assertions are needed
      // see WidgetObservable from rxandroid for example
      spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
          @Override
          public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
              String item = (String) parent.getItemAtPosition(position);
              selectSubject.onNext(item);
          }

          @Override
          public void onNothingSelected(AdapterView<?> parent) {
          }
      });

      return selectSubject;
  }

In this code fragment we send selected items to newly created subject, and return it as observable. So, all observers will receive selected items.

Next step is to combine these two observables and set result email to text field.

One of the operators, suitable for our purpose is combineLatest. It's very similar to zip: it combines items from each observable and invokes function which transforms two items into one object which gets emitted. Main difference from zip is that combineLatest waits till each observable emits at least one time and then uses this item if no new items were emitted. We cannot use zip in this case, because it will wait when both login and domain values are emitted, but we need to update result text each time one of the fields is updated. For this case we use just emitted value of one of the fields and previous value of another.

Let's take a look what results combineLatest will produce for different input:

Login Domain Email
test    
rxmail.com test@rxmail.com
rxmail.io test@rxmail.io
hello hello@rxmail.io
me reactivemail.net me@reactivemail.net


You can find interactive example how combineLatest works on RxMarbles.

And here is our final code:

private void setupFields() {
    Observable.combineLatest(
      WidgetObservable.text(mLoginText, false),//false - do not emit start value
      observeSelect(mDomainSpinner),

      new Func2<OnTextChangeEvent, String, String>() {
          @Override
          public String call(OnTextChangeEvent onTextChangeEvent, String domain) {
              CharSequence userLogin = onTextChangeEvent.text();
              if (TextUtils.isEmpty(userLogin)) {
                return "";
              } else {
                  return userLogin.toString() + '@' + domain;
              }
          }
      }
    ).subscribe(new Action1<String>() {
        @Override
        public void call(String email) {
            mResultText.setText(email);
        }
    });
}

Let's compare it with old Android way:

private void setupFieldsOldWay() {
    mLoginText.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            onFieldUpdated();
        }

        @Override
        public void afterTextChanged(Editable s) {
        }
    });
    mDomainSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
            onFieldUpdated();
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {
        }
    });
}

private void onFieldUpdated() {
    String login = mLoginText.getText().toString();
    String domain = (String) mDomainSpinner.getSelectedItem();
    if (TextUtils.isEmpty(login)) {
        mResultText.setText("");
    } else {
        mResultText.setText(login + '@' + domain);
    }
}

Rx way is better because it has all code in one place and looks almost like we are working with views synchronously. With old way we have two listeners and method which updates result text field, all these methods are separated from each other and you need to look through the code, to find what's actually happening.
Plus, in Rx way there is no need to store references to fields mLogin and mDomain because they are used only when creating Observable, less state - less possible problems.

In addition RxJava provides a lot of different operators for transforming/combining observables, for example you can filter login names:

WidgetObservable.text(mLoginText, false).filter(new Func1<OnTextChangeEvent, Boolean>() {
    @Override
    public Boolean call(OnTextChangeEvent onTextChangeEvent) {
        return onTextChangeEvent.text().length() <= 8;
    }
})

Or if you need to add another field which is part of email, you can use overload of combineLatest with bigger number of arguments, while in old way you'll need to add another listener (or any other objects that triggers result field update) and modify onFieldUpdated appropriately.


Full version of this code can be found here.
Also, there is similar (or open in app) sample with two EditText fields.
Download APK to try out the sample, click this link on device to open it.


Comments

comments powered by Disqus