Sunday, August 30, 2009

Android Input Validation

Today's article will cover a quick and dirty approach to form validation for Android applications. My application has an activity that displays a form to the user for entering personal information. This information is then stored in a SQLite database. I needed a way to validate the data in the form's widgets before saving that data in the application's database. The Android framework provides some support for entering valid data into a form. This support includes hints and input filters. Hints are a handy way to help a user enter information when a widget's text is empty.

An example of using a hint in a layout:
<edittext
    android:id="@+id/owner_first_name"
    android:hint="@string/hint_required_field"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />
In the above example, @string/hint_required_field supplies the text that will be displayed in the EditText control when it is empty:



Input filters are more complex and give the developer a way of formatting the data in a editable widget as the user enters it. Android provides a couple of implementations of InputFilter: AllCaps and LengthFilter. Input filters require writing custom code to implement their functionality.

While both approaches help to ensure that valid information is being entered, there is no support for validating a required field. So while my first thought was to go down the input filter path, I decided to create my own validation class that would allow me to check if the data was present as well as correct.

The Validation Class

The validation class is very basic. All methods take an EditText widget. They can check if a widget is required to contain data and use regular expressions to check if that data is in the correct format. If the data is not in the correct format, the color of the text is changed to help the user identify the problem.

Validation class source, sans comments (Validate.java):
package com.zunisoft.critters.utility;

import java.util.regex.Pattern;

import android.graphics.Color;
import android.util.Log;
import android.widget.EditText;

import com.zunisoft.critters.R;

public class Validate {
 private static final String CLASS_TAG = "Validate";
 
 public static final int VALID_TEXT_COLOR = Color.BLACK;
 public static final int INVALID_TEXT_COLOR = Color.RED;
 
 public static boolean isEmailAddress(EditText editText, boolean required) {
  Log.d(CLASS_TAG, "isEmailAddress()");
  
  String regex = editText.getResources().getString(R.string.regex_email);
  
  return isValid(editText, regex, required);
 }
 
 public static boolean isPhoneNumber(EditText editText, boolean required) {
  Log.d(CLASS_TAG, "isPhoneNumber()");
  
  String regex = editText.getResources().getString(R.string.regex_phone);
  
  return isValid(editText, regex, required);
 }
 
 public static boolean isPostalCode(EditText editText, boolean required) {
  Log.d(CLASS_TAG, "isPostalCode()");
  
  String regex = editText.getResources().getString(R.string.regex_postal_code);
  
  return isValid(editText, regex, required);
 }
 
 public static boolean isValid(EditText editText, String regex,
   boolean required) {
  Log.d(CLASS_TAG, "isValid()");

  boolean validated = true;
  String text = editText.getText().toString().trim();
  boolean hasText = hasText(editText);

  editText.setTextColor(VALID_TEXT_COLOR);
  
  if (required && !hasText) validated = false;

  if (validated && hasText) {
   if (!Pattern.matches(regex, text)) {
    editText.setTextColor(INVALID_TEXT_COLOR);
    validated = false;
   }
  }

  return validated;
 }
 
 public static boolean hasText(EditText editText) {
  Log.d(CLASS_TAG, "hasText()");

  boolean validated = true;
  
  String text = editText.getText().toString().trim();
  
  if (text.length() == 0) {
   editText.setText(text);
   validated = false;
  }

  return validated;
 }
}
The validation regular expressions are defined in the application's strings.xml resource file.

Regular expression resources (strings.xml):
...
^([^@\\s]+)@((?:[-a-z0-9]+\\.)+[a-z]{2,})$
^\\(?\\d{3}[\\)?|-]\\d{3}[- ]\\d{4}((\\s=?)(ext\\.|x)\\d{1,4})?$
^\\d{5}(-\\d{4})?$
...

Using the Validation Class

The next step is to put the validation class to use. In the following example, a form has a number of input fields. Some of these fields are required while others are not. Some fields expect the data to be in a certain format as well. The activity uses the Android hint mechanism to help the user identify which fields are required and the format the data should be in if necessary. After the user enters the necessary information and clicks the save button, the information is validated. If the information is incorrect, the user is presented with a pop up message warning them of any problems.



Implementation example, partial class source (Owner.java):
import com.zunisoft.critters.utility.Validate;
...
@Override
public void onClick(View v) {
 Log.d(TAG, "onClick() -> clicked on " + v.getId());

 // Process the click event for the appropriate button
 switch (v.getId()) {
 case R.id.owner_save_button:
  if(save()) {
   setResult(RESULT_OK, null);
   exitActivity();
  } else {
   showDialog(ID_DIALOG_VALIDATE_ERR);
  }
  break;
 case R.id.owner_cancel_button:
  setResult(RESULT_CANCELED, null);
  exitActivity();
  break;
 }
}

protected boolean save() {
 Log.d(TAG, "save()");

 boolean saved = true;
 
 if (validated()) {
  // Actual save code omitted for clarity
 } else {
  saved = false;
 }
 
 return saved;
}
 
protected boolean validated() {
 Log.d(TAG, "validated()");
 
 boolean validated = true;
 
 if (!Validate.hasText(editFirstName)) validated = false;
 if (!Validate.hasText(editLastName)) validated = false;
 if (!Validate.hasText(editAddress)) validated = false;
 if (!Validate.hasText(editCity)) validated = false;
 if (!Validate.isPostalCode(editPostalCode, true)) validated = false;
 if (!Validate.isPhoneNumber(editDayPhone, true)) validated = false;
 if (!Validate.isPhoneNumber(editEveningPhone, false)) validated = false;
 if (!Validate.isPhoneNumber(editMobilePhone, false)) validated = false;
 if (!Validate.isEmailAddress(editEmailAddress, false)) validated = false;
 
 return validated;
}
...
Which results in following if the user enters incorrect data:



Final Notes

The solution, while not perfect, allowed me to solve most of my input validation problems. Input filters are still worth adding to make it easier for the user to enter phone numbers, zipcodes, etc. I also chose not to highlight required fields that were empty. I felt that the hint mechanism along with the validation pop up message provided sufficient information to the user to address that particular problem. My project implements model access using POJOs with a pseudo active record interface. It would be ideal if I could incorporate the validation logic into the model code. This would be very similar to how validation is implemented in Rails.