Build dynamic user interfaces with Android and XML

Collect data using an Android forms engine

A number of websites cater to non-profits that provide easily set-up and used forms for taking polls and collecting data. This tutorial introduces a simple architecture for designing similar applications for Android—dynamic user interfaces that allow non-programmers to collect data from mobile users. You will create an example forms engine in this tutorial with both server and mobile sides.

Frank Ableson, Entrepreneur, MSI Services, Inc.

W. Frank Ableson is an entrepreneur living in northern New Jersey with his wife Nikki and their children. His professional interests include mobile software and embedded design. He is the author of Unlocking Android (Manning Publications, 2010), and he is the mobile editor for Linux Magazine.



07 September 2010

Also available in Chinese Japanese Spanish

Before you start

You should be comfortable constructing Android applications with the Android SDK to get the most from this tutorial. After completion, you will have learned how to perform application-to-web server communications with HTTP(S) and how to parse XML with the DOM parser. Along the way, you will create custom and dynamic user interface layouts, multi-threaded communications, message handlers, and progress dialogs. To a lesser degree, you will learn about AndroidManifest.xml and server side scripting.

About this tutorial

Frequently used acronyms

  • API: Application Programming Interface
  • DOM: Document Object Model
  • HTML: HyperText Markup Language
  • HTTP(S): Hypertext Transfer Protocol Secure
  • IDE: Integrated development environment
  • SAX: Simple API for XML
  • SDK: Software Development Kit
  • UI: User Interface
  • URL: Uniform Resource Locator
  • XML: Extensible Markup Language

This tutorial introduces an architecture for dynamic forms for mobile data collection on Android devices. I will begin with a high level architecture and discussion of where such an application fits in the larger context of data collection. A sneak peak at the completed project, including every source file, provides a road map to where the tutorial is taking you. In cooking-show fashion, you build the application from the ground up with each Java class carefully introduced and related to other aspects of the application, most notably the data model upon which this forms engine is constructed. To conclude, you save form data to the server and look briefly at the server side of the application.

Prerequisites

Table 1 shows the tools required for this project.

Table 1. The necessary tools for the job
ToolComment
Eclipse and ADTPrimary code editor and Android Developer Tools Plugin.
Android SDKAndroid Software Developer Kit.
Web serverAny variety that supports PHP. You can easily port the script to another server environment.

I created the code samples for this tutorial on a MacBook with Eclipse 3.4.2 and Android SDK version 8, which supports the Android release labeled 2.2. The tutorial code is not leveraging any specific features of this SDK and the application should run just fine in Android versions dating back as far as 1.5. See Resources for links to all the tools.


Data collection

Let's begin with a short discussion about data collection, and how it can be easy to implement when using an Android mobile device.

An Android data collection framework

Collecting data is a task that pre-dates the computer era. Computers have become a daily staple and have revolutionized the way you think about, find, and consume information. Companies with billion dollar market-caps exist thanks to their effectiveness in storing, retrieving, and managing vast amounts of information. The databases in use today are fed by systems of varying architectures including the main frame, client server, web applications, and now mobile applications.

Physical inventory and cycle counting applications were some of the early practical applications of mobile computing. These applications were often batch data collection, where the hardware required a docking station to upload the collected information.

The market for mobile applications has come a long way since those early days, and wireless connectivity and devices are nearly ubiquitous in many cultures and markets, invading virtually every aspect of daily life.

While the means of collecting data may have become more mobile, the core aspect of data collection has not changed significantly. The user must be presented a collection of questions and have a simple means of responding. This tutorial demonstrates the construction of a simple data collection framework for Android-powered mobile phones, leveraging a dynamic metadata structure enabled by XML.


The application architecture

Before diving into the code, examine the application setting from a very high level.

Forms Engine at a glance

Take a walk through all of the aspects of the Forms Engine application. Figure 1 depicts the relation of the application to one or more servers that provide data entry forms of varying content.

Figure 1. Application architecture
Diagram of the application architecture

In Figure 1, Form 1 provides registration for Robotics Competition and Form 2 asks the user for information about his auto maintenance habit. Using HTTP(S), the forms and Android application communicate to:

  • Download the form data.
  • Present form data to the user and optional collect device-specific data, such as camera images, sound recordings, GPS location, or compass readings.
  • Collect user-supplied data.
  • Submit data to the appropriate server.

The server side of this tutorial is implemented as a pair of files: an XML document describing the form and a PHP document responsible for recording the submission of the form. The Android application is a native application written in Java code using the Android SDK and coded in Eclipse.

Table 2 shows the application source files for the complete application. You can download the zipped file containing all of these source files (see Downloads). You will go through each of these files in detail in this tutorial.

Table 2. The required application source files
FilenameComment
XmlGui.javaEntry point for Android Activity
XmlGuiForm.javaHigh level data model and methods for a form
XmlGuiFormField.javaRepresents a form field and holds the metadata for each field of a form
XmlGuiEditBox.javaImplements a text box type user interface element
XmlGuiPickOne.javaImplements a drop-down list type user interface element
RunForm.javaProcesses a form, using the above classes
main.xmlHome page of application user interface
AndroidManifest.xmlDeployment descriptor for the Android application
xmlgui1.xmlSample form for collecting Robotics competition registration
xmlgui1-post.phpPHP script for processing form submissions
xmlgui2.xmlSample form for taking survey of automotive maintenance habits

Figure 2 shows the project structure in Eclipse for the complete application as it will look at the end of this tutorial. (View a text-only version of Figure 2.)

Figure 2. Project in Eclipse
Screen capture of project structure in Eclipse

If you do not have a working Android development environment, now is a great time to install the Android tools. For more information on how to setup an Android development environment, look in Resources for links to both of the required tools, plus some introductory articles and tutorials on developing applications for Android. Having a familiarity with Android is helpful in understanding this tutorial.

Now that you have an overview of the architecture and the application, you can get started!


The project and data model

We are now ready to start the Android project in Eclipse, creating the data model and the class that will allow you to store and manage metadata for the Forms Engine application.

Creating the project

Creating Android applications starts in the familiar place: Open Eclipse and select File > New as in Figure 3.

Figure 3. Creating a new Android application
Screen capture of creating a new Android application

This step launches the Eclipse New project wizard. Select Android Project (the specialized Java environment for Android). Be sure to give the project a valid identifier (I used XMLGUI). To match the solution as described in this tutorial, under Properties, specify XML GUI as the application name and com.msi.ibm as the package name. Select the Create Activity check box and specify XmlGui for the Activity name as in Figure 4.

Figure 4. Setting up a new project
Screen capture of setting up a new project

Once the project is created, it should look very similar to the image in Figure 5. (View a text-only version of Figure 5.)

Figure 5. Android project directly upon completion of the new project wizard
Android project directly upon completion of the new project wizard

With the project now created, it is good practice to ensure that the application builds cleanly and runs in the Android Emulator. Note that sometimes the application does not build until you edit and save the Java source file. This causes the Android SDK tools to automatically generate the files in the gen folder. This causes the files in the gen folder to be automatically generated by the Android SDK tools. You can test the application if there are no entries showing up in the Problems tab of the Eclipse environment.

To test the application, create a Run Configuration, as in Figure 6. In the Android Application list, select XmlGui. Ensure the following values are present: XmlGui in the Name field, XMLGUI in the Project field, and com.msi.ibm.XmlGui in the Launch field. Click Run.

Figure 6. Run Configuration setup
Run Configuration setup

Now that the project is created, is configured, and starts properly in the Android emulator, it is time to create the XML driven data collection tool for Android.

The data model

The nuts and bolts of this application require it to present input elements to a user, collect data, validate the data, and then submit that data to a specified server location. It is worth noting that this application is set up for new records only. There are no provisions to look up an existing record for editing or deleting.

To provide enough direction to the application on how to present the data entry forms, a set of information (commonly referred to as metadata) is required. Metadata is data about data. In general terms, this application must understand a few data elements including:

  • Form Name—Human readable name of the form
  • Form Identifier—Unique identifier for this metadata collection
  • Submission URL—Where to send the collected data
  • One or more fields—These can be text, numeric, or "choose from a list" kind of fields

Virtually all kinds of questions map to one of these three types of user interface elements. For example, you can implement a check box as a Yes or No choice field. You can implement multi-select as multiple choice fields. Of course, you can extend the code shown in this tutorial as desired.

For your application, the usage scenario is as follows: You are at an event where you can register for one or more activities. You could fill out a piece of paper, or you could wait until you get home and hope that you remember to sign onto the organization's website to register. In this case, you will assume that a user will fill out a simple form on the spot from his phone by pulling up a dynamic form on an Android device, providing the entrant's first name, last name, gender, and age.

Listing 1 shows the contents of xmlgui1.xml, which represents a registration form for a Robotics club event.

Listing 1. xmlgui1.xml
<?xml version="1.0" encoding="utf-8"?>
<xmlgui>
<form id="1" name="Robotics Club Registration" 
   submitTo="http://serverurl/xmlgui1-post.php" >
<field name="fname" label="First Name" type="text" required="Y" options=""/>
<field name="lname" label="Last Name" type="text" required="Y" options=""/>
<field name="gender" label="Gender" type="choice" required="Y" options="Male|Female"/>
<field name="age" label="Age on 15 Oct. 2010" type="numeric" required="N" options=""/>
</form>
</xmlgui>

Note the following things about this XML document:

  • The XML is very simple to parse thanks to extensive use of element attributes. This approach is used because it makes extracting the data easier than multiple child elements and tags. Using attributes in this manner also keeps the download size small and aids in keeping the parse time low.
  • The submitTo attribute tells the application where to send the data once it is collected.
  • Each field element provides attributes for both a field name and a label. While these values are related, you want to keep the value of each name attribute unique from the other name attribute values so the receiving application can properly parse and process them. You must also provide an informative label value to the user as a cue to what kind of data should go into a particular field.
  • You can readily expand this approach to include default values for each field, a regex expression for validation, or a link for more information about a particular field.
  • The options field is used as a delimited list for the choice field.

With a basic familiarity with the data model, now look at the code responsible for turning this XML data into a useful application.

Representing the data

Parsing the data is a rather mechanical exercise shown later in this tutorial. Before examining the parsing process, the application needs some place to store and manage the metadata in memory. For this purpose, you have two Java classes, one for the form and one to represent the form field. Start by looking at XmlGuiForm.java in Listing 2.

Listing 2. XmlGuiForm.java
package com.msi.ibm;
import java.util.Vector;
import java.util.ListIterator;
import java.net.URLEncoder;

public class XmlGuiForm {

   private String formNumber;
   private String formName;
   private String submitTo;
   public Vector<XmlGuiFormField> fields;


   public XmlGuiForm()
   {
      this.fields = new Vector<XmlGuiFormField>();
      formNumber = "";
      formName = "";
      submitTo = "loopback"; // do nothing but display the results
   }
   // getters & setters
   public String getFormNumber() {
      return formNumber;
   }

   public void setFormNumber(String formNumber) {
      this.formNumber = formNumber;
   }


   public String getFormName() {
      return formName;
   }
   public void setFormName(String formName) {
      this.formName = formName;
   }

   public String getSubmitTo() {
      return submitTo;
   }

   public void setSubmitTo(String submitTo) {
      this.submitTo = submitTo;
   }

   public Vector<XmlGuiFormField> getFields() {
      return fields;
   }

   public void setFields(Vector<XmlGuiFormField> fields) {
      this.fields = fields;
   }

   public String toString()
   {
      StringBuilder sb = new StringBuilder();
      sb.append("XmlGuiForm:\n");
      sb.append("Form Number: " + this.formNumber + "\n");
      sb.append("Form Name: " + this.formName + "\n");
      sb.append("Submit To: " + this.submitTo + "\n");
      if (this.fields == null) return sb.toString();
      ListIterator<XmlGuiFormField> li = this.fields.listIterator();
      while (li.hasNext()) {
         sb.append(li.next().toString());
      }
   
      return sb.toString();
   }

   public String getFormattedResults()
   {
      StringBuilder sb = new StringBuilder();
      sb.append("Results:\n");
      if (this.fields == null) return sb.toString();
      ListIterator<XmlGuiFormField> li = this.fields.listIterator();
      while (li.hasNext()) {
         sb.append(li.next().getFormattedResult() + "\n");
      }

      return sb.toString();
   }

   public String getFormEncodedData()
   {
      try {
      int i = 0;
      StringBuilder sb = new StringBuilder();
      sb.append("Results:\n");
      if (this.fields == null) return sb.toString();
      ListIterator<XmlGuiFormField> li = this.fields.listIterator();
      while (li.hasNext()) {
         if (i != 0) sb.append("&");
         XmlGuiFormField thisField = li.next();
         sb.append(thisField.name + "=");
         String encstring = new String();
         URLEncoder.encode((String) thisField.getData(),encstring);
         sb.append(encstring);
      }

      return sb.toString();
      }
      catch (Exception e) {
         return "ErrorEncoding";
      }
   }
}

Here are some important items to note about the XmlGuiForm class.

  1. There are four member variables:
    • formNumber: This is the unique identifier for the server side form distribution mechanism.
    • formName: This becomes the title of the form, providing context and confirmation for the user.
    • submitTo: This is the URL for the application to submit the data entered (after validation). Alternatively this value can be a loopback. In the loopback scenario, the data is displayed to the user rather than submitted to the server. This is useful for testing purposes.
    • fields: This is a Vector class templated to hold the form's field data. Listing 3 shows the details for XmlGuiFormField.java.
  2. A series of getters and setters for each of these variables.
  3. The toString() and getFormattedResults() methods are responsible for generating human readable summarizations of the XmlGuiForm class.
  4. The getFormEncodedData() method is used when preparing data for submission to the URL indicated in the submitTo attribute.
  5. Rather than using strictly concatenated java.lang.String classes, the code employs a StringBuilder as a more memory efficient means of building the desired data strings.
  6. The URLEncoder class is leveraged to prepare data for submission to the server. This makes the data look like it was actually created by a traditional HTML form.
  7. Some potential expansions of this application include:
    • Local storage or caching of metadata to make repetitive tasks run more quickly.
    • Local storage to record data over a period of time prior to submission.
    • GPS recording—stamp each record with location data.

Now look at the construction of the XmlGuiFormField class in Listing 3.

Listing 3. XmlGuiFormField.java
package com.msi.ibm;
// class to handle each individual form
public class XmlGuiFormField {
   String name;
   String label;
   String type;
   boolean required;
   String options;
   Object obj;   // holds the ui implementation
                   // or the EditText for example


   // getters & setters
   public String getName() {
      return name;
   }
   public void setName(String name) {
      this.name = name;
   }
   public String getLabel() {
      return label;
   }
   public void setLabel(String label) {
      this.label = label;
   }
   public String getType() {
      return type;
   }
   public void setType(String type) {
      this.type = type;
   }
   public boolean isRequired() {
      return required;
   }
   public void setRequired(boolean required) {
      this.required = required;
   }
   public String getOptions() {
      return options;
   }
   public void setOptions(String options) {
      this.options = options;
   }

   public String toString()
   {
      StringBuilder sb = new StringBuilder();
      sb.append("Field Name: " + this.name + "\n");
      sb.append("Field Label: " + this.label + "\n");
      sb.append("Field Type: " + this.type + "\n");
      sb.append("Required? : " + this.required + "\n");
      sb.append("Options : " + this.options + "\n");
      sb.append("Value : " + (String) this.getData() + "\n");

      return sb.toString();
   }
   public String getFormattedResult()
   {
      return this.name + "= [" + (String) this.getData() + "]";

   }

   public Object getData()
   {
      if (type.equals("text") || type.equals("numeric"))
      {
         if (obj != null) {
            XmlGuiEditBox b = (XmlGuiEditBox) obj;
            return b.getValue();
         }
      }
      if (type.equals("choice")) {
         if (obj != null) {
            XmlGuiPickOne po = (XmlGuiPickOne) obj;
            return po.getValue();
         }
      }

      // You could add logic for other UI elements here
      return null;
   }

}

Look more closely at the XmlGuiFormField class.

  • There are six class level members:
    1. name holds the name of the field—this is the field name of the data value, analogous to a form field name in HTML or a database column name.
    2. label holds the description of the field or the value shown to the user.
    3. type indicates the flavor of user interface field to construct.
      • text means this is field is implemented with an EditText field for alphanumeric entries. This is the most common value.
      • numeric is also an EditText, but it is constrained to a numeric entry value.
      • choice makes the field a drop-down list.
    4. required is a Boolean value marking the field as required or not. If the field is required and not populated, an error message is displayed to the user when the user attempts to submit the form.
    5. options is a string value used to convey the list of available selections for a choice field. This field is available for other fields to be used as perhaps a regex expression for validation or it can be overridden to specify a default value.
    6. obj represents the user interface widget. For example, this variable holds an EditText for a text or numeric field. For a choice field, the obj member contains a spinner widget. This approach is further explained later in this tutorial.
  • As expected, these variables have a number of getters and setters.
  • The toString() and getFormattedResult() methods both leverage the getData() method, explained next.
  • In the XmlGuiFormField class, you need to manage more than one type of data, so the code needs to be explicit about how data is stored and accessed. The getData() method examines the type field and performs a type-cast on the obj field to properly interact with the stored object. If you wish to add new field types to this framework, you can expand the getData() method to support the new field type (see the comment near the end of Listing 3.

You now have a way to store and manage metadata. It is time to look at the application in action and then tie the various pieces together.


Assemble a user interface

Start by creating a form for a mobile user to enter data into.

Taking it from the top

The entry point of the application resides in XmlGui.java, as in Listing 4.

Listing 4. The application entry point: XmlGui
package com.msi.ibm;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.Button;
import android.widget.TextView;
import android.content.Intent;
import android.util.Log;
public class XmlGui extends Activity {
        final String tag = XmlGui.class.getName();
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        Button btnRunForm = (Button) this.findViewById(R.id.btnRunForm);
        btnRunForm.setOnClickListener(new Button.OnClickListener()
        {
           public void onClick(View v)
           {
                       EditText formNumber = (EditText) findViewById(R.id.formNumber);
                       Log.i(tag,"Attempting to process Form # 
                   [" + formNumber.getText().toString() + "]");
                       Intent newFormInfo = new Intent(XmlGui.this,RunForm.class);
                       newFormInfo.putExtra("formNumber", 
                   formNumber.getText().toString());
                       startActivity(newFormInfo);
           }
        });
    }
}

The user interface for the main Activity is very simple, consisting of only:

  • A label (TextView)
  • An entry box (EditText)
  • A button (Button)

The code for XmlGui Activity is very standard in nature. You inflate a layout created at design time and then define and create a button handler to implement the desired functionality (which is explained further below).

You define the user interface in the file main.xml (found in the layout subfolder of the res folder). Listing 5 shows main.xml.

Listing 5. main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >

   <TextView  
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:text="@string/Title"
       />
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >

   <EditText
       android:layout_width="100px"
       android:layout_height="wrap_content"
       android:text="1"
       android:id="@+id/formNumber"
   android:numeric="integer"/>
   <Button android:text="Run Form" android:id="@+id/btnRunForm" 
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content">
   </Button>
</LinearLayout>

</LinearLayout>

As a reminder, you can modify layouts by editing the XML directly or by using the Layout tool contained in the Android Developer Tools as in Figure 7.

Figure 7. Layout tool
Screen capture of Layout tool

To run the application, click on the home screen icon that launches the XmlGui Activity seen in Figure 8.

Figure 8. Application in action
Screen capture of application in action

When the user enters a form number and taps the Run Form button, a series of events is kicked off. Let's go through the onClick() method, line by line. Recall that the onClick() method is in the XmlGui class in Listing 4.

You get a reference to the EditText field named formNumber. The R.id.formNumber enumeration is automatically generated by the Android build tools whenever the main.xml file is saved:

EditText formNumber = (EditText) findViewById(R.id.formNumber);

Next, you put a line into the log. You can see the output of this log in the Dalvik Debug Monitor Service (DDMS) perspective in Eclipse, provided by the Android Developer Tools plugin:

Log.i(tag,"Attempting to process Form # [" + formNumber.getText().toString() + "]");

The actual implementation of the Form Engine is provided in the RunForm class, which extends the Activity class. To launch this Activity, create an Intent, explicitly identifying the RunForm class:

Intent newFormInfo = new Intent(XmlGui.this,RunForm.class);

Not only do you want to launch the RunForm Activity, but you also want to specify which form to display. To do this, add the form number to the Intent through the putExtra method:

newFormInfo.putExtra("formNumber", formNumber.getText().toString());

This value is extracted by the RunForm class, shown later.

Now that you have set up the Intent, you launch the Activity with a call to startActivity:

startActivity(newFormInfo);

With your application, the user enters the form number and clicks the Run Form button. This triggers the events described above, causing the RunForm class to process the request. Entering a form number is really just a test tool for the purposes of this tutorial. There are other means by which this triggering event can take place. More practical examples include customized links from a web page, a message pushed through Short Message Service (SMS), a location-based trigger based on proximity, or even a scanned QR code (Quick Response code).

Running the form

The RunForm class is the choreographer for this application. It is launched with a form number to process. Now examine the onCreate() method in Listing 6.

Listing 6. The onCreate() method
public class RunForm extends Activity {
    /** Called when the activity is first created. */
           String tag = RunForm.class.getName();
           XmlGuiForm theForm;
           ProgressDialog progressDialog;
           Handler progressHandler;

           @Override
    public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           String formNumber = "";
           Intent startingIntent = getIntent();
           if(startingIntent == null) {
              Log.e(tag,"No Intent?  We're not supposed to be here...");
              finish();
              return;
           }
              formNumber = startingIntent.getStringExtra("formNumber");
              Log.i(tag,"Running Form [" + formNumber + "]");
              if (GetFormData(formNumber)) {
                     DisplayForm();
          }
          else
          {
                  Log.e(tag,"Couldn't parse the Form.");
                  AlertDialog.Builder bd = new AlertDialog.Builder(this);
                  AlertDialog ad = bd.create();
                  ad.setTitle("Error");
                  ad.setMessage("Could not parse the Form data");
                  ad.show();

          }
    }
         // other methods omitted and shown later
}

As seen in the code in Listing 6, first you extract the formNumber from the Intent that triggered the Activity. Without a form number to process, this Activity has nothing to perform.

Once you extract the value, the next requirement is to connect to the server to download the form specifications. (Note that an enhancement of this approach might be to look for this form's metadata in a local cache prior to fetching the data.) Listing 7 shows the GetFormData() method.

Listing 7. The GetFormData() method
   private boolean GetFormData(String formNumber) {
   try {
      Log.i(tag,"ProcessForm");
      URL url = new URL("http://www.example.com/xmlgui" + formNumber + ".xml");
      Log.i(tag,url.toString());
      InputStream is = url.openConnection().getInputStream();
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      DocumentBuilder db = factory.newDocumentBuilder();
      Document dom = db.parse(is);
      Element root = dom.getDocumentElement();
      NodeList forms = root.getElementsByTagName("form");
      if (forms.getLength() < 1) {
         // nothing here??
         Log.e(tag,"No form, let's bail");
         return false;
      }
      Node form = forms.item(0);
      theForm = new XmlGuiForm();

      // process form level
      NamedNodeMap map = form.getAttributes();
      theForm.setFormNumber(map.getNamedItem("id").getNodeValue());
      theForm.setFormName(map.getNamedItem("name").getNodeValue());
      if (map.getNamedItem("submitTo") != null)
         theForm.setSubmitTo(map.getNamedItem("submitTo").getNodeValue());
      else
         theForm.setSubmitTo("loopback");

      // now process the fields
      NodeList fields = root.getElementsByTagName("field");
      for (int i=0;i<fields.getLength();i++) {
         Node fieldNode = fields.item(i);
         NamedNodeMap attr = fieldNode.getAttributes();
         XmlGuiFormField tempField =  new XmlGuiFormField();
         tempField.setName(attr.getNamedItem("name").getNodeValue());
         tempField.setLabel(attr.getNamedItem("label").getNodeValue());
         tempField.setType(attr.getNamedItem("type").getNodeValue());
         if (attr.getNamedItem("required").getNodeValue().equals("Y"))
            tempField.setRequired(true);
         else
            tempField.setRequired(false);
         tempField.setOptions(attr.getNamedItem("options").getNodeValue());
         theForm.getFields().add(tempField);
      }

      Log.i(tag,theForm.toString());
      return true;
   } catch (Exception e) {
      Log.e(tag,"Error occurred in ProcessForm:" + e.getMessage());
      e.printStackTrace();
      return false;
   }
}

This code is responsible for taking data from a metadata repository, in this case by downloading an XML file from a Webserver:

URL url = new URL("http://10.211.55.2/~fableson/xmlgui" + formNumber + ".xml");

Manipulate the XML data through a DOM parser, to extract the form and field elements plus attributes and store them in instances of the XmlGuiForm and XmlGuiFormField classes respectively. The bulk of this method is dedicated to the parsing and populating tasks.

Two main approaches to XML parsing are DOM and SAX. The DOM parser works by parsing a document into memory and then the application walks a Document Object Model tree to gain access to various elements of data contained in the XML. You also could also use the SAX parser model here because you build your own representation of the document by populating the two classes.

The advantage of the DOM approach for this application is that it is somewhat procedural and easy to follow the code, whereas the SAX approach requires call-back functions where only the desired data is stored. The complexity of the code written by the developer to implement the SAX callback functions can be noticeably higher than the DOM approach in some instances. Because the XML data is fully parsed in the DOM approach, it is a bit more memory intensive. For your purposes in this application, the simplicity and easy-to-follow nature of DOM was a bigger driver than memory management as the metadata form is quite small.

See Resources for an excellent reference on coding XML parsers in Android.

You have transformed the XML metadata form to Java class instances. It is now time to display the form to gather data from the user.


Gather user data

Now that you've created the main Activity screen layout, you can create user interface forms for collecting data. Here you'll create a Robotics Club Registration form and an Auto Maintenance survey form.

Using the metadata

This application hinges upon the ability of Android programmers to dynamically manipulate the user interface. Earlier in the tutorial, you examined the main.xml file, the file that defines the screen layout of the XmlGui class (the main Activity). This application would be virtually impossible in its current form if you had to always define user interface elements at design or compile time.

Thankfully, you are not constrained in that manner. The DisplayForm() method is responsible for converting the metadata into user interface elements for the purposes of collecting data. The code is essentially broken into two main functional areas: the layout of the user interface elements and then the handling of the submit button.

First, examine the layout logic. This is the code that turns the XmlGuiForm object into a real on-the-screen form. Listing 8 shows this code.

Listing 8. The layout logic
    private boolean DisplayForm()
    {

        try
        {
            ScrollView sv = new ScrollView(this);

        final LinearLayout ll = new LinearLayout(this);
        sv.addView(ll);
        ll.setOrientation(android.widget.LinearLayout.VERTICAL);

        // walk through the form elements and dynamically create them, 
        // leveraging the mini library of tools.
        int i;
        for (i=0;i<theForm.fields.size();i++) {
            if (theForm.fields.elementAt(i).getType().equals("text")) {
                    theForm.fields.elementAt(i).obj = new
                    XmlGuiEditBox(this,(theForm.fields.elementAt(i).isRequired()
                    ? "*" : "") + theForm.fields.elementAt(i).getLabel(),"");
                    ll.addView((View) theForm.fields.elementAt(i).obj);
            }
            if (theForm.fields.elementAt(i).getType().equals("numeric")) {
                    theForm.fields.elementAt(i).obj = new
                    XmlGuiEditBox(this,(theForm.fields.elementAt(i).isRequired() 
                    ? "*" : "") + theForm.fields.elementAt(i).getLabel(),"");
                    ((XmlGuiEditBox)theForm.fields.elementAt(i).obj).makeNumeric();
                    ll.addView((View) theForm.fields.elementAt(i).obj);
            }
            if (theForm.fields.elementAt(i).getType().equals("choice")) {
                    theForm.fields.elementAt(i).obj = new
                    XmlGuiPickOne(this,(theForm.fields.elementAt(i).isRequired()
                    ? "*" : "") + theForm.fields.elementAt(i).getLabel(), 
                    theForm.fields.elementAt(i).getOptions());
                    ll.addView((View) theForm.fields.elementAt(i).obj);
            }
        }


        Button btn = new Button(this);
        btn.setLayoutParams(new LayoutParams
        (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.
        WRAP_CONTENT));

        ll.addView(btn);

        btn.setText("Submit");
        btn.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View v) {
                // check if this form is Valid
                if (!CheckForm())
                {
                    AlertDialog.Builder bd = new AlertDialog.Builder(ll.getContext());
                AlertDialog ad = bd.create();
                ad.setTitle("Error");
                ad.setMessage("Please enter all required (*) fields");
                ad.show();
                return;

                }
                if (theForm.getSubmitTo().equals("loopback")) {
                    // just display the results to the screen
                    String formResults = theForm.getFormattedResults();
                    Log.i(tag,formResults);
                    AlertDialog.Builder bd = new AlertDialog.Builder(ll.getContext());
                AlertDialog ad = bd.create();
                ad.setTitle("Results");
                ad.setMessage(formResults);
                ad.show();
                return;

                } else {
                    if (!SubmitForm()) {
                        AlertDialog.Builder bd = new AlertDialog.Builder(ll.getContext());
                    AlertDialog ad = bd.create();
                    ad.setTitle("Error");
                    ad.setMessage("Error submitting form");
                    ad.show();
                    return;
                    }
                }

            }
        } );

        setContentView(sv);
        setTitle(theForm.getFormName());

        return true;

        } catch (Exception e) {
            Log.e(tag,"Error Displaying Form");
            return false;
        }
    }

You must anticipate the availability of more fields than can fit on a single screen, so use a ScrollView as the parent view or container. Within that ScrollView you employ a vertical LinearLayout to organize the various user interface widgets into a vertical column.

The approach is pretty simple:

  • You enumerate through the list of XmlGuiFormField objects contained within the fields member of the XmlGuiForm instance.
  • Depending on the type of field requested, a different user interface element is instantiated and added to the LinearLayout. You will examine the different UI widgets momentarily.

Once the UI elements are created and added to the linear layout, you assign the entire ScrollView instance to the content of this screen and assign the form name as the title of the screen. Figure 9 shows a Robotics club registration screen ready for user input. This form is the result of processing the XML data found back in Listing 1.

Figure 9. Robotics registration form in action
Screen capture of robotics registration form in action

Let's have a look at the different custom user interface widgets created for this application.

Recall that three types of data entry fields are defined for this application: text, numeric, and choice. These three types are implemented through two different custom widgets: XmlGuiEditBox and XmlGuiPickOne.

The text and numeric values are so similar you can leverage the same EditView approach, but with different input filters to switch between alpha-numeric and numeric only. Listing 9 shows the code for the XmlGuiEditBox class.

Listing 9. The XmlGuiEditBox class
package com.msi.ibm;

import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.EditText;
import android.text.method.DigitsKeyListener;

public class XmlGuiEditBox extends LinearLayout {
   TextView label;
   EditText txtBox;

   public XmlGuiEditBox(Context context,String labelText,String initialText) {
      super(context);
      label = new TextView(context);
      label.setText(labelText);
      txtBox = new EditText(context);
      txtBox.setText(initialText);
      txtBox.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams
                   .FILL_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT));
      this.addView(label);
      this.addView(txtBox);
   }

   public XmlGuiEditBox(Context context, AttributeSet attrs) {
      super(context, attrs);
      // TODO Auto-generated constructor stub
   }

   public void makeNumeric()
   {
      DigitsKeyListener dkl = new DigitsKeyListener(true,true);
      txtBox.setKeyListener(dkl);
   }
   public String getValue()
   {
      return txtBox.getText().toString();
   }
   public void setValue(String v)
   {
      txtBox.setText(v);
   }
}

The XmlGuiEditBox class extends the LinearLayout class and contains both a textual label to describe the requested input and an EditText to actually collect the entered data. All of the object initialization is done in the constructor. This may be considered bad form, but this is an exercise left to you if you're uncomfortable with that approach.

There are three other methods to discuss. The getValue() and setValue() methods do just what you would think. They are the getter and setter for interacting with the EditText field.

The third method, makeNumeric() is only called when setting up a form field type of numeric. An instance of the DigitsKeyListener is employed to filter out any non-numeric keys. The other nice thing that you get for free is the proper keyboard is shown depending on which type of XmlGuiEditBox is in use—with or without the numeric setting.

Figure 10 shows the form in action with an alpha keyboard shown because the Last Name field is set for alpha entry, in other words text.

Figure 10. Alphanumeric key entry
Screen capture of alphanumeric key entry

Figure 11 shows the numeric keyboard in use because the age field is set for the data type of numeric.

Figure 11. Numeric keyboard
Screen capture of numeric keyboard

The choice field, implemented in the user interface through the XmlGuiPickOne class, is a little different. The choice field is implemented as an Android Spinner widget. This user interface element is analogous to a drop-down list box in other programming environments, where the user must select from one of the existing choices. Figure 12 shows three instances XmlGuiPickOne widget.

Figure 12. Auto maintenance survey with three XmlGuiPickOne instances
Screen capture of auto maintenance survey with three XmlGuiPickOne instances

In this example, the data being collected is for statistical purposes, so normalizing the possible entries makes the data processing cleaner than dealing with free text entry fields. Of course, you can define the State field as a choice field if you wanted to constrain the survey to a particular geographical region.

Listing 10 shows the code for the XmlGuiPickOne class.

Listing 10. The XmlGuiPickOne class
package com.msi.ibm;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Spinner;
import android.widget.ArrayAdapter;

public class XmlGuiPickOne extends LinearLayout {
   String tag = XmlGuiPickOne.class.getName();
   TextView label;
   ArrayAdapter<String> aa;
   Spinner spinner;

   public XmlGuiPickOne(Context context,String labelText,String options) {
      super(context);
      label = new TextView(context);
      label.setText(labelText);
      spinner = new Spinner(context);
      String []opts = options.split("\\|");
      aa = new ArrayAdapter<String>( context,
           android.R.layout.simple_spinner_item,opts);
      spinner.setAdapter(aa);
      this.addView(label);
      this.addView(spinner);
   }

   public XmlGuiPickOne(Context context, AttributeSet attrs) {
      super(context, attrs);
      // TODO Auto-generated constructor stub
   }


   public String getValue()
   {
      return (String) spinner.getSelectedItem().toString();
   }

}

This class looks very similar to the XmlGuiEditBox class. The major difference is that a Spinner control is employed rather than an EditText control. Also, note that this class only implements the getValue() method. An obvious enhancement to this class is permit the user to specify a default value.

Note the use of the options member to populate the list of choices. In this code, the String containing the available choices is split into an array using a regex expression and then passed to an instance of an ArrayAdapter. The constant android.R.layout.simple_spinner_item is built-in to Android. It was not supplied in the tutorial application code. Once the adapter is set up, you assign it to the Spinner. Figure 13 shows the list of choices displayed on the screen, prompting the user for the typical number of miles between oil changes.

Figure 13. XmlGuiPickOne asking about oil changes
Screen capture of XmlGuiPickOne asking about oil changes with choices in mile intervals

Now that the user can enter data into the form, it is time to validate and submit the data.


Save and submit data

You must now create a way for users to save the data by validating it and submitting it to a server.

Saving data

It is time to jump back into the DisplayForm() method of the RunForm class. Recall that the first portion of this method is responsible for the drawing of the form. Next, you will examine the onClick() handler of the submit button, in Listing 11.

Listing 11. The onClick() handler
btn.setOnClickListener(new Button.OnClickListener() {
   public void onClick(View v) {
       // check if this form is Valid
       if (!CheckForm())
       {
           AlertDialog.Builder bd = new AlertDialog.Builder(ll.getContext());
               AlertDialog ad = bd.create();
               ad.setTitle("Error");
               ad.setMessage("Please enter all required (*) fields");
               ad.show();
               return;
       }
       if (theForm.getSubmitTo().equals("loopback")) {
           // just display the results to the screen
           String formResults = theForm.getFormattedResults();
           Log.i(tag,formResults);
           AlertDialog.Builder bd = new AlertDialog.Builder(ll.getContext());
           AlertDialog ad = bd.create();
               ad.setTitle("Results");
               ad.setMessage(formResults);
               ad.show();
               return;
       } else {
           if (!SubmitForm()) {
               AlertDialog.Builder bd = new AlertDialog.Builder(ll.getContext());
           AlertDialog ad = bd.create();
           ad.setTitle("Error");
           ad.setMessage("Error submitting form");
           ad.show();
           return;
           }
       }
   }
} );

When the submit button is selected by the user, the form entries are checked to make sure that all of the required fields are populated. If not, an AlertDialog reminds the user to populate all of the fields. Assuming the data has been entered in a satisfactory manner, it is time to submit the data.

The process of submitting the data falls into one of two camps for this tutorial application. If the submitTo field of the form has been set to the value of loopback, the values are simply echoed to the screen. This is useful for testing purposes. Once you are satisfied that the form tool is collecting data properly, it is time to point it to a server page which is responsible for recording the entries.

Listing 12 shows the CheckForm() method. This code is rather straight-forward. Each field is checked to see if it is required. If the field is required but the user has not provided the information, a flag is set. You could enhance this to provide more specific feedback to the user.

Listing 12. The CheckForm() method
private boolean CheckForm()
{
    try {
       int i;
       boolean good = true;


       for (i=0;i<theForm.fields.size();i++) {
                   String fieldValue = (String)
               theForm.fields.elementAt(i).getData();
                   Log.i(tag,theForm.fields.elementAt(i)
               .getName() + " is [" + fieldValue + "]");
                   if (theForm.fields.elementAt(i).isRequired()) {
                       if (fieldValue == null) {
                           good = false;
                       } else {
                           if (fieldValue.trim().length() == 0) {
                               good = false;
                           }
                       }

                   }
       }
       return good;
    } catch(Exception e) {
       Log.e(tag,"Error in CheckForm()::" + e.getMessage());
       e.printStackTrace();
       return false;
    }
}

Now it is time to submit the collected data to the server. Examine the SubmitForm() method in Listing 13.

Listing 13. The SubmitForm() method
private boolean SubmitForm()
{
       try {
                   boolean ok = true;
       this.progressDialog = ProgressDialog.show(this, 
         theForm.getFormName(), "Saving Form Data", true,false);
       this.progressHandler = new Handler() {

               @Override
               public void handleMessage(Message msg) {
                   // process incoming messages here
                   switch (msg.what) {
                       case 0:
                           // update progress bar
                           progressDialog.setMessage("" + (String) msg.obj);
                           break;
                       case 1:
                           progressDialog.cancel();
                           finish();
                           break;
                       case 2:
                               progressDialog.cancel();
                               break;
                   }
                   super.handleMessage(msg);
               }

       };

       Thread workthread = new Thread(new TransmitFormData(theForm));

       workthread.start();

               return ok;
       } catch (Exception e) {
               Log.e(tag,"Error in SubmitForm()::" + e.getMessage());
               e.printStackTrace();
       // tell user that the submission failed....
       Message msg = new Message();
       msg.what = 1;
       this.progressHandler.sendMessage(msg);

               return false;
       }

}

First, you set up an instance of the android.os.Handler class. The Handler class is helpful when an application needs to share data with different threads. Another important item to note in the SubmitForm() method is the use of a ProgressDialog. Note that the ProgressDialog and Handler are defined as class level variables in Listing 14.

Listing 14. The ProgressDialog and Handler
public class RunForm extends Activity {
    /** Called when the activity is first created. */
      String tag = RunForm.class.getName();
      XmlGuiForm theForm;
      ProgressDialog progressDialog;
      Handler progressHandler;
     ...
}

You don't want to unnecessarily block the application while communicating with the server, so you employ a background Thread to communicate, but you rely on the Handler to receive notifications from the communications thread in order to provide feedback to the user. Note that only the main thread is supposed to interact with the user interface. An alternative to the separate thread approach is the AsyncTask class found in the android.os package.

As the application connects to the server to transfer the data, it has the opportunity to inform the user of the status of the operation, which is of course good practice. Figure 14 shows the ProgressDialog in action.

Figure 14. The ProgressDialog
Screen capture of The ProgressDialog

The actual server communications code is found in Listing 15, in the TransmitFormData() class, which implements the Runnable interface.

Listing 15. The TransmitFormData class
   private class TransmitFormData implements Runnable
   {
   XmlGuiForm _form;
   Message msg;
   TransmitFormData(XmlGuiForm form) {
       this._form = form;
   }

   public void run() {

       try { 
                msg = new Message();
                msg.what = 0;
                msg.obj = ("Connecting to Server");
                progressHandler.sendMessage(msg);

                URL url = new URL(_form.getSubmitTo());
                URLConnection conn = url.openConnection();
                conn.setDoOutput(true);
                BufferedOutputStream wr = new BufferedOutputStream
                       (conn.getOutputStream());
                String data = _form.getFormEncodedData();
                wr.write(data.getBytes());
                wr.flush();
                wr.close();
 
                msg = new Message();
                msg.what = 0;
                msg.obj = ("Data Sent");
                progressHandler.sendMessage(msg);

                // Get the response
                BufferedReader rd = new BufferedReader(new
                 InputStreamReader(conn.getInputStream()));
                String line = "";
                Boolean bSuccess = false;
                while ((line = rd.readLine()) != null) {
                       if (line.indexOf("SUCCESS") != -1) {
                           bSuccess = true;
                       }
                       // Process line...
                       Log.v(tag, line);
                }
                wr.close();
                rd.close();

                if (bSuccess) {
                       msg = new Message();
                       msg.what = 0;
                       msg.obj = ("Form Submitted Successfully");
                       progressHandler.sendMessage(msg);

                       msg = new Message();
                       msg.what = 1;
                       progressHandler.sendMessage(msg);
                       return;

                }
       } catch (Exception e) {
                Log.d(tag, "Failed to send form data: " + e.getMessage());
                msg = new Message();
                msg.what = 0;
                msg.obj = ("Error Sending Form Data");
                progressHandler.sendMessage(msg);
       }
       msg = new Message();
       msg.what = 2;
       progressHandler.sendMessage(msg);
   }

   }

The TransmitFormData class is responsible for connecting to the server listed in the submitTo member of the XmlGuiForm instance (as taken from the metadata). It periodically updates the main application thread by sending an instance of a Message class to the handler through the sendMessage() method. Two members are populated on the Message class:

  • The what value acts as a high-level switch informing the Handler what it should do with the message.
  • The obj value specifies an optional java.lang.Object. In this case, a java.lang.String instance is passed and used for displaying in the Progress Dialog.

The schema used by any given application is arbitrary. This application uses the values in Table 3.

Table 3. The application values allowed for what
ValueComment
0Obj contains a text string to display to the user.
1Successful completion of transmission, you're done!
2An error occurred. Tell the user that something is wrong and don't throw away the data!

Figure 15 shows the final message in the ProgressDialog upon a successful transmission of Form Data.

Figure 15. Form submission
Screen capture of form submission message

Once the form has been successfully submitted, the application returns to the main page. For a production-ready application, what takes place next is highly dependent on the motives of the data gathering organization. The screen can simply reset to take another entry, as in a physical inventory application. Or perhaps you can direct the user to some other screen.

For the application to run properly, the AndroidManifest.xml file must contain references to all of the used Activity classes and must include the uses-permission for Internet access. Listing 16 shows the AndroidManifest.xml file for the tutorial's application.

Listing 16. The AndroidManifest.xml file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.msi.ibm"
      android:versionCode="1"
      android:versionName="1.0">
      <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".XmlGui"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".RunForm">
        </activity>
    </application>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>

</manifest>

Before wrapping up, take a brief look at the server side script.


Provide a server side script

For the purposes of this tutorial, you will use a PHP script to gather the required data, and append it to a text file.

On the server

Exactly what transpires on the server is dependent on the needs of the organization collecting the data. A common approach for data collection is to store the form data in a relational database such as DB2®, MySQL, SQL Server, Oracle, and so on. Once the data is in the database, it can be sliced, diced, and analyzed.

For this tutorial, the data is gathered by a PHP script and appended to a text file. Listing 17 shows the PHP form associated with the Robotics registration form.

Listing 17. The Robotic's PHP form
<?php
// xmlgui form # 1
// this page is expecting
// fname
// lname
// gender
// age


$filename = "/pathtowritablefile/datafile.txt";


$f = fopen($filename,"a");
fprintf($f,"Data received @ ".date(DATE_RFC822));
fprintf($f,"\n");
fprintf($f,'First Name:['.$_POST['fname'].']');
fprintf($f,"\n");
fprintf($f,'Last Name:['.$_POST['lname'].']');
fprintf($f,"\n");
fprintf($f,'Gender:['.$_POST['gender'].']');
fprintf($f,"\n");
fprintf($f,'Age:['.$_POST['age'].']');
fprintf($f,"\n");
fclose($f);
print "SUCCESS";
?>

If the script returns the string SUCCESS, the RunForm class will reset. Any other value will cause an error message to be displayed to the user and permit them to correct their entries or otherwise obtain help in submitting the form.


Conclusion

This tutorial presented a framework for serving dynamic questions to an Android user based on a native application approach utilizing the Android SDK. You saw dynamic form presentation, validation and processing techniques, and application-to-web server communications.


Download

DescriptionNameSize
Forms engine source codeformsengine.zip78KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into XML on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML, Web development, Industries, Mobile development
ArticleID=516603
ArticleTitle=Build dynamic user interfaces with Android and XML
publish-date=09072010