Reactive Forms with RxAndroid

Reactive Programming has been getting a lot of attention in the Android community lately. While it has uses throughout the application stack, we're going to focus here on using it to validate forms (exciting!). This approach cuts down on ugly nested if statements and transforms all of the validation logic to just a few simple lines using the RxJava framework. More, it's robust and testable.

 

Reactive Forms with RxAndroid

This post assumes some knowledge of how RxJava and lambdas work. If you need more of a refresher RxJava Retrolambda

 

Setup layout with TextInputLayout

 

Image of gif

 

Lets start by setting up the input fields in our layout. The TextInputLayout from the Design Support Library will handle animating our hint and displaying our in-line error messages. We just need to wrap our EditText in the TextInputLayout and it will handle the rest for you.

 

    <android.support.design.widget.TextInputLayout
        android:id="@+id/credit_card_input_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"

        <EditText
            android:id="@+id/credit_card_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="CreditCard"/>

    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:id="@+id/email_input_layout"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:layout_below="@id/input_layout"
        android:layout_marginTop="20dp">

        <EditText
            android:id='@+id/email_input'
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:hint="Email"/>

    </android.support.design.widget.TextInputLayout>

 

Image of gif

 

Setup in-line error messages

Now let’s setup an observable on our EditTexts. RxAndroid has a built in method for doing this, RxTextView.textChanges(). It takes a EditText and returns an Observable emmitting a CharSequence on each character change.

    RxTextView.textChanges(mCreditCardInput);

Now we have an Observable on text changes in the EditText, so let’s get to the actual validating. We want to match a regex with Strings passing through the observable and display an in-line error message if the text is not a valid credit card number. A great way to do this is to map the String to a boolean value representing ‘is valid’, then show or hide the error message based on that boolean.

    RxTextView.textChanges(mCreditCardInput)
            .map(inputText -> inputText.toString().matches("credit card regex here");

Next we want to make sure it doesn’t show an error when no text is entered, so we add a little bit of logic to the validation map operation

    Observable<Boolean> creditCardObservable = RxTextView.textChanges(mInputField)
            .map(inputText -> (inputText.length() == 0) || inputText.toString().matches("credit card regex here"));

Now we have an observable that watches the input field and emits a boolean value of whether the input is valid or not. Next let’s subscribe and update our TextInputLayout wrapper. TextInputLayout has setErrorEnabled and setError methods which will show or hide an in-line error message.

    mCreditCardInputLayout.setError("Invalid Credit Card Number);
    RxTextView.textChanges(mCreditCardInput)
            .map(inputText -> (inputText.length() == 0) || inputText.toString().matches("credit card regex here"));
            .subscribe(isValid -> mCreditCardInputLayout.setErrorEnabled(!isValid));

 

Image of gif

 

Let’s clean this up a little bit by breaking out the observable and subscribing seperately for each operation that needs to happen. We add a filter to the observable controlling the error messaging to only show if there is an error.

    mInputLayout.setError("Invalid Credit Card Number")
    Observable<Boolean> creditCardObservable = RxTextView.textChanges(mCreditCardInput)
            .map(inputText -> (inputText.length() == 0) || inputText.toString().matches("credit card regex here"))
            .distinctUntilChanged();

    creditCardObservable.subscribe(isValid -> mInputLayout.setErrorEnabled(!isValid));

We added the distinctUntilChanged() operator to the chain. It constrains the final value to be emitted only if it is different from the previously emitted value.

Now, let’s repeat that except for an Email address input.

    mCreditCardInputLayout.setError("Invalid Email);
    Observable<Boolean> emailObservable = RxTextView.textChanges(mEmailInput)
            .map(inputText -> (inputText.length() == 0) || inputText.toString().matches("credit card regex here"))
            .distinctUntilChanged();

    emailObservable.subscribe(isValid -> mCreditCardInputLayout.setErrorEnabled(isValid))

 

Setup submit button

Now we have out in-line error messages working. Next we need to combine these observables and manipulate them in a way that lets us know when both fields are validated so we can enable a submit button. Luckily, RxJava has a combineLatest operator which works perfectly in this scenero. We only have two observables here but this technique could be used for as many fields as your form requires.

    Observable.combineLatest(
                    creditCardObservable,
                    emailObservable,
                    (creditValid, emailValid) -> creditValid && emailValid)
                    .distinctUntilChanged()
                    .subscribe(valid -> mSubmit.setEnabled(valid));

 

Image of gif

 

So what is happening here? Let’s go through it step by step. combineLatest is an RxJava operator. From the ReactiveX documenation:

when an item is emitted by either of two Observables, combine the latest item emitted by each Observable via a specified function and emit items based on the results of this function

We are going to use it by combining the values emitted by our observables and &&'ing them all in order to get one final item emitted which represents the state of all the observable validation.

    .subscribe(valid -> mSubmit.setEnabled(valid));

Finally, enable or disabled the button based on the value emitted.

Gotchas

  • combineLatest will not emit a value until all of the observables combined emit at least one value.

  • These obsevables on text changes would be considered “Hot Observables”. Because of this, we have to be careful about unsubscribing with activity lifecycle changes in order to avoid leaking a context. One easy way of doing this is to use RxLifecycle. More info here

  • If text is entered or deleted too quickly ‘Backpressure’ issues can surface. More info on backpressure

  • If you don’t want the inline error to show up immediately before a user has finished typing, consider the debounce operator. It allows you to set a delay on when items are emitted from an observer. Setting a 1 second debounce allows the user to type the next character before the error is displayed effectively delaying the error until the typing is completed.

This post just scratches the surfact of whats possible with this framework. Here is some further reading on what else can be accomplished with reactive streams:

https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/

 

Share