Keeping it clean

Implementing text watcher validations on all your edit texts in Android.

The Android design support library provides a consistent way of displaying errors on input fields that steadily more apps are starting to use; to the extent that Android apps that display their errors in a different way often feel quite jarring.

The key to this consistent display of errors comes by wrapping all your edit texts in a text input layout. This also, as a bonus, delivers the material design style floating labels that liven up what is often the most boring part of the user experience in an app.

I’m like a kid at Christmas every time a new version [of the Android support library] is released: I rush downstairs to see what new toys Santa has delivered, only to discover that my new train set is missing some parts and Santa broke some of my favourite toys and trampled soot into the carpet whilst he was delivering it.

In this article I’ll be discussing how you can create a common reusable pattern for implementing validation across all the fields on your input form. Since you want to hide the errors once the user has corrected the error I’m a big believer in implementing the validation by using TextWatchers.

Unfortunately, in the latest release of the support library (23.1), there is a bug that stops the errors reappearing once you have hidden them, so this example is built against the older 23.0.1 support library. I have a love-hate relationship with the support library at the moment — I’m like a kid at Christmas every time a new version is released: I rush downstairs to see what new toys Santa has delivered, only to discover that my new train set is missing some parts and Santa broke some of my favourite toys and trampled soot into the carpet whilst he was delivering it.

Creating our common class

My minor grumbles aside, let’s create an abstract ErrorTextWatcher class that implements the TextWatcher interface. For our simple example I’m saying that our TextWatcher always takes a TextInputLayout and it has a single error message that can be displayed. It’s possible that your UX team might want to display different errors — e.g. “Password cannot be empty”, “Password must contain at least one number”, “Please enter at least 4 characters” etc.— but for the sake of simplicity I’ll show how to implement a single message per TextWatcher.

public abstract class ErrorTextWatcher implements TextWatcher {

private TextInputLayout mTextInputLayout;
private String errorMessage;

protected ErrorTextWatcher(@NonNull final TextInputLayout textInputLayout, @NonNull final String errorMessage) {
this.mTextInputLayout = textInputLayout;
this.errorMessage = errorMessage;
}

I’ve also added some convenience methods to the abstract class:

public final boolean hasError() {
return mTextInputLayout.getError() != null;
}

protected String getEditTextValue() {
return mTextInputLayout.getEditText().getText().toString();
}

I also want all my ErrorTextWatchers to implement a validate() method that returns true if the input is okay and an easy way to show or hide the error:

public abstract boolean validate();

protected void showError(final boolean error) {
if (!error) {
mTextInputLayout.setError(null);
mTextInputLayout.setErrorEnabled(false);
} else {
if (!errorMessage.equals(mTextInputLayout.getError())) {
// Stop the flickering that happens when setting the same error message multiple times
mTextInputLayout.setError(errorMessage);
}
mTextInputLayout.requestFocus();
}
}

There is another feature of the library here that I’ve coded around: in my opinion you should just be able to hide the error by setting error enabled to false but this can leave the underline beneath the edit text as the wrong colour so you also need to set the error message to empty to get the underline reset. Likewise if you continually set the error string to the same value, the error message will keep flickering with an animation, so only overwrite it if it has a new value.

Finally, I’ve been a bit naughty in requesting the focus on the edit text within the TextWatcher — hopefully you’ll see why I did this when you see how I validate the input form, but for your needs you may want to move this logic elsewhere.

As an additional optimisation, I find that I can implement all my logic within the onTextChanged method of the TextWatcher interface so I’ve added two empty methods for the beforeTextChanged and afterTextChanged to the parent class.

Validating for minimum length

Now let’s create a concrete example of this class. A common use case is that an input field needs to be at least x characters. So let’s create a MinimumLengthTextWatcher. This takes a minimum length value and, of course, the TextInputLayout and message that I need for the parent class. Additionally, I don’t want to keep telling the user that they need to enter x characters before they’ve even finished typing — that would be a bad UX — so we should only start showing the error once the user has already exceeded the minimum limit.

public class MinimumLengthTextWatcher extends ErrorTextWatcher {

private final int mMinLength;
private boolean mReachedMinLength = false;

public MinimumLengthTextWatcher(final TextInputLayout textInputLayout, final int minLength) {
this(textInputLayout, minLength, R.string.error_too_few_characters);
}

public MinimumLengthTextWatcher(final TextInputLayout textInputLayout, final int minLength, @StringRes final int errorMessage) {
super(textInputLayout, String.format(textInputLayout.getContext().getString(errorMessage), minLength));
this.mMinLength = minLength;
}

There are two constructors here: one with a default message and a second where you could create a more specific one for a particular text field. Since we want to support localisation, we use Android string resources rather than hard-coded String values.

Our text changed and validate methods are now easily implemented as follows:

@Override
public void onTextChanged(final CharSequence text…) {
if (mReachedMinLength) {
validate();
}
if (text.length() >= mMinLength) {
mReachedMinLength = true;
}
}

@Override
public boolean validate() {
mReachedMinLength = true; // This may not be true but now we want to force the error to be shown
showError(getEditTextValue().length() < mMinLength);
return !hasError();
}

You’ll note that once the validate method has been called on the TextWatcher that it will always display the error. I think this works for most use cases but you may want to introduce a setter to reset this behaviour for certain scenarios.

You now need to add the text watcher to your text input layout following the creation of your views within your activity or fragment. Here you simply say:

mPasswordView = (TextInputLayout) findViewById(R.id.password_text_input_layout);
mValidPasswordTextWatcher = new MinimumLengthTextWatcher(mPasswordView, getResources().getInteger(R.integer.min_length_password));
mPasswordView.getEditText().addTextChangedListener(mValidPasswordTextWatcher);

You can then check whether a field is valid at the appropriate point in your code with:

boolean isValid = mValidPasswordTextWatcher.validate();

If the password isn’t valid, the view will automatically get focus and the screen should scroll to that point.

Validating email addresses

Another common validation use case is to check whether an email address is validly formed or not. I could easily write a whole article on the correct regular expression for an email address but since this is often contentious I’ve separated the email validation logic from the text watcher itself. The sample project includes a testable EmailAddressValidator that you can use or you can implement your own logic as you wish.

Since I’ve split the email validation logic out, the ValidEmailTextWatcher is remarkably similar to the MinimumLengthTextWatcher:

public class ValidEmailTextWatcher extends ErrorTextWatcher {

private final EmailAddressValidator mValidator = new EmailAddressValidator();
private boolean mValidated = false;


public ValidEmailTextWatcher(@NonNull final TextInputLayout textInputLayout) {
this(textInputLayout, R.string.error_invalid_email);
}

public ValidEmailTextWatcher(@NonNull final TextInputLayout textInputLayout, @StringRes final int errorMessage) {
super(textInputLayout, textInputLayout.getContext().getString(errorMessage));
}

@Override
public void onTextChanged(…) {
if (mValidated) {
validate();
}
}

@Override
public boolean validate() {
showError(!mValidator.isValid(getEditTextValue()));
mValidated = true;
return !hasError();
}

Hooking the text watcher up within our activity or fragment is exactly the same:

mEmailView = (TextInputLayout) findViewById(R.id.email_text_input_layout);
mValidEmailTextWatcher = new ValidEmailTextWatcher(mEmailView);
mEmailView.getEditText().addTextChangedListener(mValidEmailTextWatcher);

Bringing it all together

For a registration or sign in form you would normally validate all your fields before you submit them to your api. Since I request the focus on any views that fail the validation in the text watcher, I generally validate the views from the bottom up. In this way the app displays the errors for all fields that need correcting but jumps to the first erroneous input text on the form. E.g.

private boolean allFieldsAreValid() {
/**
* Since the text watchers automatically focus on erroneous fields, do them in reverse order so that the first one in the form gets focus
* &= may not be the easiest construct to decipher but it's a lot more concise. It just means that once it's false it doesn't get set to true
*/
boolean isValid = mValidPasswordTextWatcher.validate();
isValid &= mValidEmailTextWatcher.validate();
return isValid;
}

You can find an example of all the above code with a working example for a login screen on GitHub here. This is a child branch of the ClearableEditText repository that I described in https://medium.com/engineering-at-depop/giving-your-edit-texts-the-all-clear but it works just as well with standard EditTexts. It also includes a few more tricks and bug workarounds that I didn’t have time to mention here.

Although I’ve only shown two examples of text watchers, hopefully you can see how easily you can now add other text watchers with different validations to any text input that requires validation and reuse them throughout your app.

As usual, if you enjoyed this post, I’d appreciate a recommend, follow, share or tweet.

We’re hiring!

Do you want to join the Depop family? We’re looking for Android engineers who can help change the way people buy and sell from their mobile using the latest android UI, coding and testing practices.

Find our vacancies here and get in touch with a copy of your CV.