Build dynamic user interfaces with Android and XML

Before you start

To get the most from this tutorial, you should be comfortable constructing Android applications using the Android SDK. On 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. You’ll also learn about AndroidManifest.xml and server-side scripting.

About this tutorial

This tutorial introduces an architecture for dynamic forms for mobile data collection on Android devices. It begins with a high-level architecture and discussion of where such an application fits in the larger context of data collection. You’ll take a sneak peek at the completed project, including every source file, to give you a roadmap to where the tutorial is taking you. In cooking-show fashion, you’ll 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 on which the forms engine is constructed. To conclude, you’ll save form data to the server and look briefly at the server side of the application.

Prerequisites

Table 1 shows the tools that are required for this project.

Table 1. The necessary tools for the job

Tool Comment
Android Studio Integrated development environment (IDE) for constructing Android applications.
Android SDK Android Software Developer Kit, managed by Android Studio.
Web server Any 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 Android Studio 3.2.1 and Android SDK 27, which supports the Android release 8.1 (“Oreo”). The tutorial code does not require any specific features of this SDK, and the application should run fine in Android versions dating back as far as 1.5. See “Resources” for links to all the tools.

Android data collection framework

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

Collecting data is a task that predates the computer era. Computers have revolutionized the way that we think about, find, and use 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 populated by systems of varying architectures, including the mainframe, 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 often implemented 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 geographies and markets, pervading virtually every aspect of daily life.

While the means of collecting data might have become more mobile, the core aspect of data collection has not changed significantly. The user must be presented with 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, using a dynamic metadata structure enabled by XML.

Application architecture: Forms Engine at a glance

Before diving into the code, let’s look at the Forms Engine application from a high level. Figure 1 shows 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 metadata
  • Present the form to the user and optionally 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
  • A PHP document responsible for recording the submission of the form

The Android application is a native application that is written in the Java language using the Android SDK and coded in Android Studio.

Table 2 shows the application source files for the complete application. You can download the .zip file containing all of these source files (see “Downloadable resources”). This tutorial reviews each of these files in detail.

Table 2. The required application source files

File name Comment
XmlGui.java Entry point for Android activity
XmlGuiForm.java High level data model and methods for a form
XmlGuiFormField.jav Represents a form field and holds the metadata for each field of a form
XmlGuiEditBox.java Implements a text box type user interface element
XmlGuiPickOne.java Implements a drop-down list type user interface element
RunForm.java Processes a form, using the above classes
activity_xml_gui.xml Home page of the application user interface
AndroidManifest.xml Deployment descriptor for the Android application
xmlgui1.xml Sample form for collecting Robotics Competition registration
xmlgui1-post.php PHP script for processing form submissions
xmlgui2.xml Sample form for taking a survey of automotive maintenance habits

Figure 2 shows the project structure in Android Studio for the complete application as it looks at the end of this tutorial.

Figure 2. Project in Android Studio
Screen capture of project structure in Android Studio

This is a great time to install the Android Studio if you do not have a working Android development environment. For more information on how to set up an Android development environment, see “Resources” for links to both of the required tools, plus some introductory articles and tutorials on developing applications for Android.

Now that you’ve reviewed the architecture and the application, it’s time to get started.

Project and data model

You are now ready to start the Android project in Android Studio, creating the data model and the class that lets you store and manage metadata for the Forms Engine application.

Creating the project

To create a project, complete the following steps:

  1. Open Android Studio and click File > New, as shown in Figure 3.

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

    The Android Studio “New project” wizard opens.

  2. Click Project and name the project (I used “XMLGUI”).

  3. Select the version of the devices that you want your project to target. I am choosing Android 81.1 (Oreo), as shown in Figure 4.

    Figure 4. Name your project
    Naming a new project

  4. Choose which Android version that you want to target with your application. As shown in Figure 5, I’ve chosen Android 8.1 (Oreo).

    Figure 5. Target Android devices
    Determine which Android version you want to target with your application

  5. Select Empty Activity, as shown in figure 6.

    Figure 6. Select Empty Activity
    Select Empty Activity

  6. Name your activity. (In my example, I chose “XmlGui.”) Your associated layout file is named activity_xml_gui.xml, as shown in Figure 7.

    Figure 7. Naming your activity
    Naming your activity

    When the project is created, it should look similar to the image shown in Figure 1.

Now that the project is created, you can use the Android Emulator to ensure that the application builds cleanly and runs as expected. In the Android Studio menu, click the Build button (the hammer icon), and then click the Run button (the green play icon). The build steps generate the required intermediate files and prepare the application to run on either a real device or an instance of the Android emulator. See “Resources” for help on the basics of building and running Android applications with Android Studio.

When the project has been created, configured, and starts properly in the Android emulator, it is time to create the XML-driven data collection tool for Android.

Data model

This application presents input elements to a user, collects data, validates the data, and then submits that data to a specified server location. Note that the application is set up for new records only. There are no provisions to look up an existing record for editing or deleting.

To provide sufficient direction to the application on how to present the data entry forms, you must use a set of information called metadata, which is simply data about data. This application must understand a few data separate elements, including:

  • Form Name: The human-readable name of the form
  • Form Identifier: The 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” types of fields

Virtually all types of questions map to one of these three types of user interface elements. For example, you can implement a checkbox as a Yes or No choice field. You can also implement multiple choice fields. You can extend the code that is shown in this tutorial to suit your purposes.

For your new 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 wait until you get home and hope that you remember to sign onto the organization’s website to register. However, your application enables a user to complete a simple form on the spot from an Android phone by pulling up a dynamic form and providing their first name, last name, gender, and age.

Listing 1 shows the contents of the xmlgui1.xml file, 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 about this XML document:

  • The XML is simple to parse due to extensive use of element attributes. This approach makes extracting the data easier than multiple child elements and tags. Using attributes in this way also keeps the download size small and helps keep the parse time low.

  • The submitTo attribute tells the application where to send the data after it is collected. If you are building this application yourself, you’ll want to replace serverurl with the path to your own web server, equipped with the ability to interpret PHP script files.

  • Each field element provides attributes for both a field name and a label. While these values are related, keep the value of each name attribute unique from the other name attribute values so that 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 belongs in 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 attribute provides a delimited list for a choice field.

Now that you’re familiar with the data model, let’s look at the code responsible for turning this XML data into a useful application.

Representing the data

Parsing the data is a somewhat mechanical exercise that I’ll cover later in this tutorial. Before you look at the parsing process, the application needs a 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.navitend.xmlgui;

import android.util.Log;

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"; // ie, 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();
            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();
                String rawString = (String) thisField.getData();
                encstring = URLEncoder.encode(rawString);
                sb.append(encstring);
                i++;
            }
            return sb.toString();
        }
        catch (Exception e) {
            e.printStackTrace();
            return "ErrorEncoding " + e.getMessage();
        }
    }


}

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

  1. There are four member variables:
    • formNumber: This variable is the unique identifier for the server-side form distribution mechanism.
    • formName: This variable becomes the title of the form, providing context and confirmation for the user.
    • submitTo: This variable 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. There are 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 wanted data strings.
  6. The URLEncoder class is used 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 before submission
    • GPS recording to stamp each record with location data

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

Listing 3. XmlGuiFormField.java

package com.navitend.xmlgui;

import com.navitend.xmlgui.XmlGuiEditBox;

// 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 , i.e. 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();
            }
        }

        // todo, add other UI elements here
        return null;
    }

}

Looking more closely at the XmlGuiFormField class:

  • There are six class-level members:
    1. name holds the name of the field, which 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 that is shown to the user.
    3. type indicates the type of user interface field to construct.
      • text means that this field is implemented with an EditText field for alphanumeric entries. This is the most common value.
      • numeric is also an EditText field, 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 that is used to convey the list of available selections for a choice field. This field is available for other fields to be used as, possibly, 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 field for a text or numeric field. For a choice field, the obj member contains a spinner widget. This approach is explained later in this tutorial.
  • As expected, these variables have a number of getters and setters.
  • The toString() and getFormattedResult() methods both use the getData() method, which is explained next.
  • In the XmlGuiFormField class, you need to manage more than one type of data, so the code must 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 want 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’s 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.

Application entry point

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

Listing 4. The application entry point: XmlGui

package com.navitend.xmlgui;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

public class XmlGui extends AppCompatActivity {
    final String tag = XmlGui.class.getName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_xml_gui);

        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 consists of:

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

The code for the XmlGui activity is standard. You inflate a layout that is created during design and then define and create a button handler to implement the wanted function (which is described below).

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

Listing 5. activity_xml_gui.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>

Remember, you can modify layouts by editing the XML directly or by using the Layout tool that is contained in Android Studio, as shown in Figure 8.

Figure 8. Layout tool
Screen capture of Layout tool

You can now build and run the application. Android Studio prompts you to choose either a connected device or a configured Android Virtual Device, as shown in Figure 9.

Figure 9. Select a device to test the application
Select a device to test the application

The application is displayed in the emulator, as shown in Figure 10.

Figure 10. Application in action
Application in emulator

When the user enters a form number and taps the Run Form button, a series of events is started. Let’s review 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 activity_xml_gui.xml layout 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 LogCat window in Android Studio.

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

The actual implementation of the Form Engine is provided in the RunForm class, which is a separate activity. To start this activity, create an intent, explicitly identifying the RunForm class.

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

Not only do you want to start 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, which is shown later.

Now that you have set up the intent, you start the activity with a call to startActivity.

startActivity(newFormInfo);

In your application, the user enters the form number and clicks the Run Form button. This triggers the events described previously, 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. Other 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 Quick Response (QR) code.

Running the form

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

Listing 6. The onCreate() method

@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 + "]");
        String url = this.getString(R.string.fetchformurl) + formNumber + ".xml";
        new GetFormData().execute(url);
    }

You first extract the formNumber from the intent that triggered the activity. Without a form number to process, this activity has nothing to perform.

After you extract the value, the next requirement is to connect to the server to download the form specifications. (Note that an enhancement to this approach might be to look for this form’s metadata in a local cache before fetching the data.) To download the data from the server, you must perform the network request on a background thread because you are not permitted to perform this type of operation on the main UI thread. You also need a server URL to access. Note that this is the kind of constant value that you want to store outside of the source code. For this application, you store it in the res/strings.xml file with a reference identifier of fetchformaturl. The getString() method retrieves this value for you. Listing 7 shows the GetFormData() class, which extends the AsyncTask class that performs the actual download of your form metadata.

Listing 7. The GetFormData() class

private class GetFormData extends AsyncTask<String, Void, Boolean> {

        @Override
        protected Boolean doInBackground(String... strings) {
            String formData = null;
            try {
                String formURL = strings[0];
                URL url = new URL(formURL);
                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 (IOException e) {
                e.printStackTrace();
                return false;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }

        @Override
        protected void onPostExecute(Boolean result) {
            if (result) {
                DisplayForm();
            } else {
                Log.e(tag,"Couldn't parse the Form.");
                finish();
                Toast.makeText(getApplicationContext(), "Form not valid!",
                        Toast.LENGTH_LONG).show();
            }
        }
    }

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

In the doInBackground method, you extract the passed-in URL by referencing the first string value and passing it to the URL class constructor.

String formURL = strings[0];
URL url = new URL(formURL);

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

You can use two main approaches to XML parsing: DOM and SAX. The DOM parser works by first parsing a document into memory and then having the application walk a Document Object Model tree to gain access to various elements of data contained in the XML. You can 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 wanted data is stored. Code to implement the SAX callback functions can be more complex 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 this application, the simplicity and easy-to-follow nature of DOM is a bigger driver than memory management because the metadata form is quite small.

See “Resources” for alternative approaches to 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.

Assuming a successful parsing of the XML form metadata, you return true in the doInBackground method. The onPostExecute method handles the next step where you either call the DisplayForm method or display an error message and close this activity.

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 depends on the ability of Android programmers to dynamically manipulate the user interface. Earlier in the tutorial, you examined the activity_xml_gui.xml file, which defines the screen layout of the XmlGui class (the main activity). This application would be virtually impossible to implement in its current form if you had to always define user interface elements at design or compile time.

Fortunately, you are not constrained that way. 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 the handling of the Submit button.

First, examine the layout logic. This is the code that turns the XmlGuiFormobject 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 straightforward:

  • You enumerate through the list of XmlGuiFormField objects that are 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’ll examine the different UI widgets momentarily.

When 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 11 shows a Robotics club registration screen that is ready for user input. This form is the result of processing the XML data found in Listing 1.

Figure 11. 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 that you can use the same EditView approach, but with different input filters to switch between alphanumeric and numeric only. Listing 9 shows the code for the XmlGuiEditBox class.

Listing 9. The XmlGuiEditBox class

package com.navitend.xmlgui;

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 could 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 exactly what you would assume, acting as the getter and setter for interacting with the EditText field.

The third method, makeNumeric(), is only called when setting up a numeric form field type. An instance of the DigitsKeyListener is employed to filter out any nonnumeric keys. Another benefit is that the proper keyboard is shown depending on which type of XmlGuiEditBox is in use — with or without the numeric setting.

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

Figure 12. Alphanumeric key entry

Screen capture of alphanumeric key entry

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

Figure 13. Numeric keyboard

Screen capture of numeric keyboard

The choice field, which is 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 of the XmlGuiPickOne widget.

Figure 14. 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.navitend.xmlgui;

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 to 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, and was not supplied in the tutorial application code. Once the adapter is set up, you assign it to the spinner. Figure 15 shows the list of choices displayed on the screen, prompting the user for the typical number of miles between oil changes.

Figure 15. XmlGuiPickOne asking about oil changes

XmlGuiPickOne asking about oil changes

Now that the user can enter data into the form, it’s 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 drawing the form. Next, take a look at the onClick() handler of the submit button, as shown 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 they are not all populated, an AlertDialog reminds the user to populate all of the fields. Assuming the data has been entered correctly, it’s time to submit the data.

The process of submitting the data falls into one of two categories 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. When you are satisfied that the form tool is collecting data properly, it’s time to point it to a server page, which is responsible for recording the entries.

Listing 12 shows the CheckForm() method, which is straightforward. Each field is checked to see whether it is required. If the field is required but the user has not provided the information, a flag is set. You can enhance this method 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’s 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;
       }

}

Earlier, when you downloaded the form’s metadata, you used an extension of the AsyncTask class. This time, you’re going to take a slightly different approach by using a ProgressDialog and explicitly creating a separate Thread to submit the data to the server. Let’s have a look.

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;
     ...
}

Because you don’t want to unnecessarily block the application while communicating with the server, you use a background Thread to communicate, but you rely on the Handler to receive notifications from the communications thread to provide feedback to the user. You need to take this extra step because only the main thread is supposed to interact with the user interface.

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 a good practice. Figure 14 shows the ProgressDialog in action.

Figure 16. 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 ProgressDialog.

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

Table 3. The application values allowed for what

Value Comment
0 Obj contains a text string to display to the user.
1 Successful completion of transmission. You’re done.
2 An error occurred. Tell the user that something is wrong and don’t throw away the data.

Figure 17 shows the final message in the ProgressDialog upon a successful transmission of form data.

Figure 17. Form submission

Screen capture of form submission message

After 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.navitend.xmlgui "
      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 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 depends on the needs of the organization that is 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, or Oracle. When 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 that is 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 resets. Any other value causes an error message to be displayed to the user and permits them to correct their entries or otherwise obtain help in submitting the form.

Important security note

Never permit data to be uploaded to your server in a place where someone can subsequently run it. In Listing 17, make sure that the path that you are writing to is in a temporary folder that is not accessible from the public internet.

Conclusion

This tutorial presented a framework for serving dynamic questions to an Android user based on a native application approach using the Android SDK. By completing the tutorial, you’ve learned about dynamic form presentation, validation and processing techniques, and application-to-web server communications.