Sun's Java 2 Micro Edition (J2ME) allows developers to create applications destined for a variety of handheld devices, from the traditional PDA to so-called smart phones. To introduce you to J2ME development using the Mobile Information Device Profile (MIDP) specification, I'll be walking you through a complete J2ME application, called Expenses, in this article series.
Last time, I showed you the main MIDlet for Expenses and introduced you to two lcdui user interface components, Command and List. In this article, I'll show you the detail form used to edit an existing expense item or add a new one and let you get your feet wet with the Item family of components.
Feeling a little lost?
You might want to refer to these previous Builder.com articles before proceeding:
"Making sense of the J2ME alphabet soup"
"Getting wireless with J2ME MIDP development"
"Building and testing J2ME MIDP applications with the Wireless Toolkit"
"Exploring J2ME: Building an expense tracker"
Now, please return your tray tables and seatbacks to their full upright and locked position, and let's get started, shall we? Go ahead and download the updated source code for Expenses and then take a look at the new DetailForm form, which is displayed in the DefaultGrayPhone emulator shown in Figure A.
|The new detail form|
Let's examine the new form first and then move on to the modifications I made to the Expenses class to hook DetailForm into the application.
Designing the new form
DetailForm extends the Form base class (which itself extends Screen, so it can contain other components) and implements both the commandListener and ItemStateListener interfaces, allowing it to handle its own events. If you've looked at the code already, you may have noticed a setDestroyListener method in DetailForm's API, which accepts an instance of a class implementing the custom DestroyListener interface. By imitating the event notification mechanism used by lcdui like this, I can notify DetailForm's controlling MIDlet that the user has finished using the form. When the MIDlet receives this event, it can use Display.setCurrent to activate another Screen component.
Why go to that much trouble? Because I want to be able to use DetailForm from more than one MIDlet, but more on that later. First, let's talk about Items.
Checking out the constructor
DetailForm's constructor appears in Listing A. You can see that it takes as a parameter an instance of ExpenseInfo, which allows me to handle editing an existing item and creating a new item with the same code base. DetailForm sets its title through a call to the base Form constructor and then creates four Item UI components—a DateField, two TextFields, and a ChoiceGroup—to display the info for an ExpenseItem, plus a couple of Commands.
As we all know by now, the Item family of components descends from the Item abstract class, and, like most family members, the components have a couple of things in common. First, Items can be placed on Screen objects. Second, they share a single event, itemStateChange, which is raised to signal some change in the data the component represents.
The uses for the DateField and TextField components should be fairly obvious from their names. A DateField displays a date and allows a user to select a new date via a familiar calendar interface. DateFields have several modes, which are set by passing one of three static DateField constants as the constructor's second parameter:
- · DateField.DATE_TIME sets the DateField to display both date and time.
- · DateField.DATE sets the component to display and edit only the date (no time).
- · DateField.TIME displays and edits the time only (no date).
Since we're concerned only with dates in Expenses I create the dfDateDateField component as date-only, which you can see in Listing A.
The TextField component displays and allows editing of text, similar to a text box control in a desktop application. TextFields support some rudimentary input constraints, which are set when the component is created by the final constructor argument. In addition to restricting input, these constraints can simplify data entry with a phone-style number pad by recognizing only certain specific characters for a given key (only numbers for a numeric field, for example). The possible constraint values are:
- · TextField.ANY: Anything goes; text, special characters, and numeric values are all okay.
- · TextField.EMAILADDR: Only characters that can make up valid e-mail addresses are allowed.
- · TextField.NUMERIC: Only numeric values are allowed. Remember that there is no support for floating-point numbers in MIDP, so this constraint will allow only whole numbers.
- · TextField.PASSWORD: This provides the traditional "replace everything the user enters with an asterisk (*)" treatment for passwords.
- · TextField.PHONENUMBER: Only entries that would make valid parts of a phone number are allowed.
- · TextField.URL: Only characters that could be legally contained in a URL are allowed.
The fmDetail form contains two TextFields—tfDesc, which allows entry of a text description of an event (e.g., "Lunch"), and tfAmount, which records the dollar amount of the expense and so is created with the TextField.NUMERIC constraint.
The detail form's cgCategory component is a ChoiceGroup that allows the user to place an expense into one of several preset expense categories: Meals, Lodging, Car, Entertainment, or Miscellaneous. ChoiceGroups are similar in functionality to List components, which I introduced last time, but they support only the EXCLUSIVE and MULTIPLE formats. (There's no such thing as an IMPLICIT ChoiceGroup.) Similarly, being Item components, ChoiceGroups fire itemStateChanged events when they are manipulated, while Lists fire commandAction events.
There are a couple of ways to add an item to a ChoiceGroup. First, you can construct an empty component and add items via the append or insert methods. Alternatively, you can pass a String array containing the items you want the ChoiceGroup to display to the overloaded constructor. Either method works fine, but I use the second one here, primarily to give me the option of storing the list of categories in a central location (as a static array member of ExpenseInfo). That'll make it easier to add new categories or give the user the ability to do so in the future.
It's also possible to specify an icon for each item in the ChoiceGroup. Doing so here would needlessly complicate things though, so I'll leave that for a future article.
The Items on DetailForm all raise an itemStateChanged event when a user manipulates their data. As you can see in Listing B, itemStateChanged works in a similar fashion as the commandAction event: It examines the Item that raised the event to determine what to do with it. In this case, DetailForm updates its private instance of ExpenseItem to match whatever data has been modified by the user.
The only thing that bears more explanation here is the handling of tfAmount's change event. Not having floating-point numbers available obviously complicates things when dealing with a dollar and cents amount, like I am here. There are two ways to solve this: provide separate TextFields for the dollars and cents portion of an amount or always assume that the final two digits will represent the cents. While this requires the user to always record amounts with cents (100 for one dollar instead of just 1), I think it's a fair trade off, given the complexity of the alternative.
Of course, DetailForm also handles commandAction events raised by its two Commands, cmOK and cmCancel. When a user invokes either, DetailForm calls DestroyListener.requestDestroy. If the user invoked cmOK, the private ExpenseInfo instance is told to save the changes made to it.
Hooking everything together
Now all we have left to do is modify the Expenses MIDlet to use DetailForm. That requires three steps:
- 1. Implement DestroyListener in Expenses so that it can destroy DetailForm and show lsMain when a user closes it via one of its two commands.
- 2. Modify Expenses.commandAction (Listing C) so that when a user selects an item in lsMain, it is displayed in DetailForm.
- 3. Modify Expenses.commandAction so that when a user invokes cmAdd, DetailForm is created and shown with a new, blank ExpenseInfo instance.
Design decision: Why two MIDlets?
When you look at the code, you'll notice that there are now two MIDlets, the original Expenses, plus a new NewExpense MIDlet. The reason for this is simple: Most of the time, users of Expenses will be opening the app simply to add a new expense item. This decision reflects two fundamental principles of mobile application design: Make things as easy as possible for the user and provide shortcuts for often-performed tasks.
NewExpense simply duplicates the behavior that occurs when the cmAdd command is invoked in Expenses. It creates a new, blank instance of ExpenseInfo and a new instance of DetailForm to display it. Of course, at this point, the app still isn't backed by a data store, so any new item added through NewExpense will not appear in Expenses. Don't fret too much, though; we'll be doing that soon enough.