//TODO Introduction
This post assumes some knowledge of how RxJava and lambdas work. If you need more of a refresher RxJava Retrolambda
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>
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);
}
});
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))
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));
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.
-
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