Skip to content

Instantly share code, notes, and snippets.

@wmbest2
Forked from DavidRyan/rxandroidinput.md
Last active September 14, 2015 14:03
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 wmbest2/51b3824fc276d34cc47a to your computer and use it in GitHub Desktop.
Save wmbest2/51b3824fc276d34cc47a to your computer and use it in GitHub Desktop.

Reactive Forms with RxAndroid and TextInputLayout

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, its robust and testable.

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, 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 and functional programming this is pretty simple.

    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 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/

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