As soon as you start writing apps that require user input such as a web form, you will wonder "Are there any form processing libraries" for Android? You will then google that question. And you will probably find a handful of them.

There is a general principle for handing form fields which most of these frameworks would have implemented. This general approach has the following key points.

Use inputType attribute in layout xml files

Android has a built-in capacity to categorize fields when you put them the layout files. Here is an example


<EditText android:id="@+id/email"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:inputType="textEmailAddress"
    />

Notice the use of inputType as a textEmailAddress. This specification will allow android to appropriately allow or disallow based on what is permitted in this field. There are a number of input types that you could make use of. If you have eclipse you can press ctrl-space and you will see all of the available types

Use TextView.setError()

Android also provides a nice mechanism to set an error message on a text view field if you realize there is a problem.

This view will look like this when you do setError()

What is the problem then?

consider the following button click function of an activity that requires a number of input fields


public void signupButtonClick(View v)
{
    //get the following fields
    userid
    password1
    password2
    email
    
    //Validate the fields
    But how?
    
    //call a signup function
    signup(userid,password1,email);
}

Before calling the signup() you have to validate the fields. Doing so for each field whether it is required and what other rules need to apply is laborious and could quickly become a spaghetti.

Code up a validator framework

The you typically solve thsi problem is you can write validators and attach them to each field. You can do this through metadata java annotations as done in a number of these frameworks or or attach those validators to the fields through code. Once you have these fields with validators attached to them, you can treat them as a set of validators adn each the whole set one time and proceed with taking the action only if the validation is successful. The validation step would also have used the setError() to indicate messages for each invalid field. Now you can write the above code like this


MyActivity extends FormActivity
{
    //Keep local pointers to your fields that you need    
    EditText userid;
    EditText password1;
    EditText password2;
    EditText email
    
    //This is a callback from the base class FormActivity
    @Override
    protected void initFields()
    {
        //use findViewById() to populate the local fields
        //attach validators
       
        //Attach a suitable validator to the field
        Field useridField = new Field(userid,required=true);
        userid.addValueValidator(
            new MinMaxValidator(5,10,"Must be between 5 and 10"));
            
        //Add the field to the form
        addValidator(useridField);
        //or
        addValidator(new Field(password1));//default required validator
        addValidator(new Field(password2));//default required validator
        addValidator(new Field(email));//default validator
        
        //add a compound validator
        addValidator(new PasswordRule(password1,password2));
    }
    
    //button click
    public void signupButtonClick(View v)
    {
        //Form Activity will have a single method to do this
        if (validateForm() == false)
        {
            alert("Sorry fill in the indicated fields");
        }
        
        //All good to go
        //Read the following fields
        userid
        password1
        password2
        email

        //call a signup function
        signup(userid,password1,email);
    }
}

Here is what the FormActivity looks like

Here is the source code for the FormActivity and its supporting classes.



public interface IValidator {
    public boolean validate();
}

public interface IValueValidator 
{
    boolean validateValue(String value);
    String getErrorMessage();
}

public class Field
implements IValidator
{
     private TextView control;
     private boolean required = true;
     private ArrayList<IValueValidator> valueValidatorList 
             = new ArrayList<IValueValidator>();

     public Field(TextView tv)
     {
         this(tv, true);
     }
     public Field(TextView tv, boolean inRequired)
     {
         control = tv;
         required = inRequired;
     }
    @Override
    public boolean validate() 
    {
        String value = getValue();
        if (StringUtils.invalidString(value))
        {
            //in valid string
            if (required)
            {
                warnRequiredField();
                return false;
            }
        }
        for(IValueValidator validator: valueValidatorList)
        {
            boolean result = validator.validateValue(getValue());
            if (result == true) continue;
            if (result == false)
            {
                //this validator failed
                String errorMessage = validator.getErrorMessage();
                setErrorMessage(errorMessage);
                return false;
            }
        }//eof-for
        //All validators passed
        return true;
    }//eof-validate
    private void warnRequiredField()
    {
        setErrorMessage("This is a required field");
    }
    public void setErrorMessage(String message)
    {
        control.setError(message);
    }
    public String getValue()
    {
        return this.control.getText().toString();
    }
}//eof-class

public class PasswordFieldRule implements IValidator
{

    private TextView password1;
    private TextView password2;
    
    public PasswordFieldRule(TextView p1, TextView p2)
    {
        password1 = p1;
        password2 = p2;
    }
    @Override
    public boolean validate() 
    {
        String p1 = password1.getText().toString();
        String p2 = password2.getText().toString();
        if (p1.equals(p2))
        {
            return true;
        }
        //They are not the same
        password2.setError("Sorry, password values don't match!");
        return false;
    }
}//eof-class

public class FormActivity 
extends Activity
{
    //Called back by setContentView
    protected abstract void initializeFormFields();
    
    @Override
    public void setContentView(int viewid) {
        super.setContentView(viewid);
        initializeFormFields();
    }
    
    //List of validators
    private ArrayList<IValidator> ruleSet = new ArrayList<IValidator>();
    
    //Ability to add validators
    public void addValidator(IValidator v)
    {
        ruleSet.add(v);
    }
    
    //Run the validations
    public boolean validateForm()
    {
        boolean finalResult = true;
        for(IValidator v: ruleSet)
        {
            boolean result = v.validate();
            if (result == false)
            {
                finalResult = false;
            }
            //if true go around
            //if all true it should stay true
        }
        return finalResult;
    }

}//eof-class

Conclusion

Of course this is a very basic idea. You can copy this code and extend it to suit your needs. This took me an hour to write. So I am sure you will need more. You can even coopt the code for annotations if you like. Or extend the idea using delegation instead of an inheritable FormActivity. This later approach will allow you to have differetn rule sets for different submit buttions.

satya - 3/8/2013 1:51:42 PM

See this link for the research that lead to this framework

See this link for the research that lead to this framework

You might find at this link if there are more complete frameworks/libraries that suit your needs better.

satya - 3/8/2013 1:55:10 PM

Some of you might benefit from this base activity class

Adjust this class to your needs. And have your form activity extend this class. This class lets you do common alerts, toasts, and progress dialogs much more easily. I haven't cleaned up this code at all. So morph this to suit your most common needs.


public abstract class BaseActivity extends Activity
implements IReportBack
{
    //private variables set by constructor
   private static String tag=null;
   private ProgressDialog pd = null;
   
   public BaseActivity(String inTag)
   {
      tag = inTag;
   }
   
   public void reportBack(String message)
   {
      reportBack(tag,message);
   }
   public void reportTransient(String message)
   {
      reportTransient(tag,message);
   }
   
   public void reportBack(String tag, String message)
   {
      Log.d(tag,message);
   }
   public void reportTransient(String tag, String message)
   {
      String s = tag + ":" + message;
      Toast mToast = Toast.makeText(this, s, Toast.LENGTH_SHORT);
      mToast.show();
      reportBack(tag,message);
      Log.d(tag,message);
   }
   public boolean invalidString(String s)
    {
       return !validString(s);
    }
    public boolean validString(String s)
    {
       if (s == null)
       {
          return false;
       }
       if (s.trim().equalsIgnoreCase(""))
       {
          return false;
       }
       return true;
    }   
   public void gotoActivity(Class activityClassReference)
   {
      Intent i = new Intent(this,activityClassReference);
      startActivity(i);
   }
   
   //Utility functions
   public void turnOnProgressDialog(String title, String message)
   {
      pd = ProgressDialog.show(this,title,message);
   }
   public void turnOffProgressDialog()
   {
      pd.cancel();
   }
   
   public void alert(String title, String message)
   {
      AlertDialog alertDialog = new AlertDialog.Builder(this).create();
      alertDialog.setTitle(title);
      alertDialog.setMessage(message);
      alertDialog.setButton("OK", new DialogInterface.OnClickListener() {
         public void onClick(DialogInterface dialog, int which) {
         }
      });
      alertDialog.show();      
   }
}//eof-class