Skip to content

Instantly share code, notes, and snippets.

@DavidRyan
Created June 18, 2015 19:18
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/4d2888091cfc08206adc to your computer and use it in GitHub Desktop.
Save DavidRyan/4d2888091cfc08206adc to your computer and use it in GitHub Desktop.

Form Validation and In-line Errors with RxAndroid and TextInputLayout

Functional Reactive Programming in Android using RxAndroid has been talked about a lot recently in the Android community. It is great for for cleaning up a variety of aspects of Android programming, but in this post I'm going to focus on form validation (exciting, right?). The problem that needs to be solved is showing in-line error messages on a payment form and enabling a button if all input forms check out. Observing character changes on the EditTexts, using the new Design Support Library's InputFieldLayouts, and making use of some nifty RxAndroid operators makes solving this problem pretty simple. We'll be using the RetroLambda plugin to give us lambda support for RxJava.

(http://i.imgur.com/SouI7G6.gif)

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

Setup layout with textInputLayout

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>

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. Now let's get to the actual validating. We want to match a regex to the 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, this is as easy as adding a filter operation.

    .filter(inputText -> inputText.length() > 0);

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 a setErrorEnabled method which will show or hide an in-line error message.

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

Let's clean this up a little bit by breaking out the observable and subscribing seperately for each operation that needs to happen.

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

    creditCardObservable.subscribe(isValid -> mCreditCardInputLayout.setErrorEnabled(!isValid))
    creditCardObservable.subscribe(isValid -> mCreditCardInputLayout.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());
            .filter(inputText -> inputText.length() > 0);
            .map(inputText -> inputText.matches("email regex here");

    emailObservable.subscribe(isValid -> mCreditCardInputLayout.setErrorEnabled(!isValid))
    emailObservable.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.

    Observable.combineLatest(
                    creditCardObservable,
                    emailObservable,
                    (values -> Observable.from(values).all(x -> x == true))
                    .flatMap(a -> a)
                    .distinctUntilChanged()
                    .subscribe(valid -> mSubmit.setEnabled(valid));

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 booleans emitted by both of our observables and && them all in order to get one final item emitted which represents the state of all the observables validation combined.

    (values -> Observable.from(values).all(x -> x == true))

This is just a fancy way of calling (creditCardValid, emailValid) -> (creditCardValid && emailValid) that is not dependent on how many observables are used since combineLatest can take up to 9.

    .flatMap(a -> a)

Map's the stream from Observable to just Boolean

    .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

  • If text is entered or deleted to quickly 'Backpressure' issues can arrise. To fix this consider adding a backpressurebuffer to your observables.

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