Skip to content

Instantly share code, notes, and snippets.

@DavidRyan
Created August 26, 2015 18:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DavidRyan/9fc1a486207e5365ad67 to your computer and use it in GitHub Desktop.
Save DavidRyan/9fc1a486207e5365ad67 to your computer and use it in GitHub Desktop.

Reactive Forms with RxAndroid and TextInputLayout

//TODO Introduction

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. You just need to wrap your 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, ViewObservable.text(). It takes a EditText and returns an Observable emmitting a TextView on each character change. We'll need to map the observable stream to a String but with RxJava this is no problem.

    ViewObservable.text(mCreditCardInput)
            .map(textView -> textView.getText().toString());

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.

    ViewObservable.text(mCreditCardInput)
            .map(textView -> textView.getText().toString());
            .map(inputText -> inputText.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 = ViewObservable.text(mInputField)
            .map(textView -> textView.getText().toString())
            .map(inputText -> (inputText.length() == 0) || inputText.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.

    ViewObservable.text(mCreditCardInput)
            .map(textView -> textView.getText().toString());
            .map(inputText -> (inputText.length() == 0) || inputText.matches("credit card regex here"));
            .subscribe(isValid -> {
                mCreditCardInputLayout.setErrorEnabled(!isValid);
                if (!isValid) {
                    mCreditCardInputLayout.setError("Invalid Credit Card Number);
                }
            });

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.

    Observable<Boolean> creditCardObservable = ViewObservable.text(mCreditCardInput)
            .map(textView -> textView.getText().toString());
            .map(inputText -> (inputText.length() == 0) || inputText.matches("credit card regex here"));

    creditCardObservable.subscribe(isValid -> mInputLayout.setErrorEnabled(!isValid));
    creditCardObservable.filter(isValid->!isValid).subscribe(isValid -> mInputLayout.setError("Invalid Credit Card Number"));

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

    Observable<Boolean> emailObservable = ViewObservable.text(mEmailInput)
            .map(textView -> textView.getText().toString());
            .map(inputText -> (inputText.length() == 0) || inputText.matches("credit card regex here"));

    emailObservable.subscribe(isValid -> mCreditCardInputLayout.setErrorEnabled(isValid))
    emailObservable.filter.(isValid->!isValid).subscribe(isValid -> mCreditCardInputLayout.setError("Invalid Email))

Setup submit button

Great. 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.

    .distinctUntilChanged()

This constrains the final value to be emitted only if it is different from the previously emitted value.

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

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

Gotchas

  • 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 a CompositeSubsctiption. More info here

  • If text is entered or deleted to quickly 'Backpressure' issues can arrise causing crashes. 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:

blah
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment