Navigator is an example of a Menu-Driven user interface. Another common UI is the form or control panel. For example, a stack calculator UI is a form containing a text field that displays the top of the stack, and six buttons labeled +, *, -, /, PUSH, and POP. Pressing one of the arithmetic buttons causes the top two numbers on the stack to be replaced by their sum, product, difference, or integer quotient. The PUSH button pushes the number entered in the Top field onto the top of the stack. The POP button removes the top most number on the stack.
Here are screen shots showing the result of 53 PUSH, 12 PUSH, and 5 PUSH:
The stack now looks like this:
(5, 12, 53)
The following screen shots show the result of +, *, -:
Pressing + then * causes the following changes to the stack:
(5, 12, 53) -> (17, 53) -> (901)
Pressing the - button causes an error, because there aren't two numbers left on the stack to be replaced.
The Calculator midlet not only implements the CommandListener interface, it also implements the ItemCommandListener interface. This is the interface for all objects that can handle the commands fired by items on a form.
public class Calculator extends MIDlet
implements CommandListener, ItemCommandListener
{
// components:
private Display display;
private Form calcForm;
private TextField numField;
private CalcBean calcStack = new
CalcBean();
// add controls to calcForm:
private void addButton(String name,
boolean ln) { ... }
public Calculator() { ... }
// lifecycle methods:
protected void startApp() {
display.setCurrent(calcForm);
}
protected void destroyApp(boolean
unconditional) { }
protected void pauseApp() { }
// command handlers:
public void commandAction(Command c,
Displayable d) {
if (c.getCommandType() ==
Command.EXIT) {
destroyApp(false);
notifyDestroyed();
}
}
public void commandAction(Command c,
Item item) { ... }
} // Calculator
The constructor creates a new form, calcForm, then uses the Form.append() method to add items such as text fields and buttons to the form:
Calculator() {
display = Display.getDisplay(this);
calcForm = new Form("Stack
Calculator");
numField = new TextField("Top:
", "", 50, TextField.DECIMAL);
calcForm.append(numField);
addButton("+", false);
addButton("*", false);
addButton("-", false);
addButton("/", true);
addButton("PUSH", false);
addButton("POP", true);
calcForm.addCommand(
new Command("Exit",
Command.EXIT, 1));
calcForm.setCommandListener(this);
}
Creating and adding buttons is done by a helper method called addButton(). This method creates a StringItem in the form of a button. The command fired by this button will be a custom item command which can be identified by its label, name. The midlet will be the listener for this command. The item is given some layout flags before it is added to the form:
void addButton(String name, boolean ln) {
Command cmmd
= new Command(name, Command.ITEM,
1);
StringItem item = new
StringItem(name, null, Item.BUTTON);
item.setDefaultCommand(cmmd);
item.setItemCommandListener(this);
int flags
= Item.LAYOUT_2 | Item.LAYOUT_CENTER
| Item.LAYOUT_EXPAND;
if (ln) flags |=
Item.LAYOUT_NEWLINE_AFTER;
item.setLayout(item.getLayout() |
mask);
calcForm.append(item);
}
Another variant of commandAction() must be implemented by ItemCommandListeners. Our implementation is based on a multi-way conditional that uses the command's label to determine which low-level handler should be invoked:
void commandAction(Command c, Item item) {
try {
if
(c.getLabel().equals("+")) calcStack.add();
else if
(c.getLabel().equals("*")) calcStack.mul();
else if
(c.getLabel().equals("-")) calcStack.sub();
else if
(c.getLabel().equals("/")) calcStack.div();
else if (c.getLabel().equals("POP"))
calcStack.pop();
else if
(c.getLabel().equals("PUSH")) {
Integer a1 =
Integer.valueOf(numField.getString());
calcStack.push(a1);
AlertType.INFO.playSound(display);
}
} catch (Exception e) {
String gripe = "Error: " +
e;
Alert a = new
Alert("Error", gripe, null, AlertType.ERROR);
display.setCurrent(a);
}
}
A midlet may use helper objects (erroneously called beans to emphasize its reusability). Our Calculator midlet uses a calculator bean to handle all of the application-specific logic and data:
class CalcBean extends java.lang.Stack {
private Integer arg1, arg2;
private void setArgs() throws
EmptyStackException {
arg1 = (Integer)pop();
try {
arg2 = (Integer)pop();
} catch (EmptyStackException ese) {
push(arg1); // restore stack to
previous state
throw ese; // rethrow exception
}
}
public void add() throws
EmptyStackException {
setArgs();
push(new Integer(arg1.intValue() +
arg2.intValue()));
}
public void mul() throws EmptyStackException
{
setArgs();
push(new Integer(arg1.intValue() *
arg2.intValue()));
}
public void sub() throws
EmptyStackException {
setArgs();
push(new Integer(arg1.intValue() -
arg2.intValue()));
}
public void div() throws Exception {
setArgs();
if (arg2.intValue() == 0)
throw new Exception("Can't
divide by 0!");
push(new Integer(arg1.intValue() /
arg2.intValue()));
}
} // CalcBean
Notice that the numbers stored on the stack are Integers. That's because many PDAs don't support floating point arithmetic.
The Calculator midlet instantiates the Model-View-Controller design pattern.
In this pattern the Model component (CalcBean) is responsible for storing application data (the stack of numbers) and implementing application logic (add, mul, push, etc.) The model does not know about the View and Controller components.
Controller components are responsible for receiving and handling user input commands. In out example the Calculator midlet performs this task.
View components are responsible for user output. The calcForm plays this role in our example. In general, displayables (screens and canvases) are views.
MIDP platforms provide persistence in the form of sequential-access record stores. A record is an array of bytes. In the following example we store pairs of names and phone numbers. The user interface provides typical browser commands:
Here are the necessary includes:
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.rms.*;
import java.io.*;
A DAO decouples an application from the technology used to access secondary memory. It does this by presenting simple read/write operations to the client, while encapsulating the low-level access details. Our DAO assumes the record store is named "Addresses". An array of 64 bytes serves as our temporary storage for a single record:
class AddressDAO {
private RecordStore rs;
public String rsName =
"Addresses";
private byte[] buffer = new
byte[64];
int nextID = 1;
public AddressDAO() {
try {
rs = RecordStore.openRecordStore(
rsName, true);
} catch(Exception ioe) {
System.err.println("--->
" + ioe);
}
}
public void write(String s) { ... }
public String read() { ... }
public void close() { ... }
public void delete() { ... }
}
Each record in a store is identified by a unique record ID number. (Stores are hash tables). This id number is returned by the addRecord() method:
void write(String s) {
int pos = 0;
int rID = -1;
buffer[pos] = (byte)'S'; // string
token
byte[] temp = s.getBytes();
for(int i = 0; i < s.length();
i++) {
buffer[pos++] = temp[i];
}
try {
rID = rs.addRecord(buffer, 0,
pos);
} catch (RecordStoreException rse) {
System.err.println("--->
" + rse);
try {
rs.deleteRecord(rID);
} catch (Exception e) {
System.err.println("--->
" + e);
}
}
}
The read() method reads the "next" record in the store, skipping over ID numbers of deleted records. It restarts when it reaches the end of the store:
String read() {
String str = "";
try {
int count = rs.getNumRecords();
do {
if (nextID ==
rs.getNextRecordID())
nextID = 1; // restart
buffer = rs.getRecord(nextID++);
} while (count-- > 0
&& buffer == null);
str = new String(buffer);
} catch(Exception e) {
System.err.println("--->
" + e);
}
return str;
}
The delete() method deletes the current record:
void delete() {
try {
if (nextID ==
rs.getNextRecordID())
nextID = 1;
rs.deleteRecord(nextID++);
} catch (Exception e) {
System.err.println("--->
" + e);
}
}
void close() {
try {
rs.closeRecordStore();
// rs.deleteRecordStore(rsName);
} catch (Exception e) {
System.err.println("--->
" + e);
}
}
The Address Book midlet uses a single form for displaying the name and phone of the current record:
public class AddressBook extends MIDlet
implements CommandListener,
ItemCommandListener {
// pre-defined commands:
private final static Command CMD_EXIT
= new Command("Exit",
Command.EXIT, 1);
// display & displayables:
private Display theDisplay;
private Form recForm;
private TextField nameField, numField;
private AddressDAO dao = new
AddressDAO();
// build & display form:
private void addButton(String name) {
... }
public AddressBook() { ... }
// Lifecycle methods:
protected void destroyApp(boolean
unconditional) {
dao.cloase();
}
protected void pauseApp() { }
protected void startApp() {
theDisplay.setCurrent(recForm);
}
// command handlers:
public void commandAction(Command c,
Displayable d) { ... }
public void commandAction(Command c,
Item item) {... }
}
Here is how the form is constructed:
AddressBook() {
theDisplay =
Display.getDisplay(this);
recForm = new Form("Address
Book");
nameField = new
TextField("Name: ", "", 50, TextField.ANY);
numField
= new TextField("Phone:
", "", 50, TextField.PHONENUMBER);
recForm.append(nameField);
recForm.append(numField);
addButton("Next");
addButton("Save");
addButton("Del");
recForm.addCommand(
new Command("Exit",
Command.EXIT, 1));
recForm.setCommandListener(this);
}
The constructor uses a private helper method:
void addButton(String
name) {
Command cmmd
= new Command(name, Command.ITEM,
1);
StringItem item = new
StringItem(name, null, Item.BUTTON);
item.setDefaultCommand(cmmd);
item.setItemCommandListener(this);
int mask
= Item.LAYOUT_2 |
Item.LAYOUT_CENTER | Item.LAYOUT_EXPAND;
item.setLayout(item.getLayout() |
mask);
recForm.append(item);
}
The item command handler simply calls the appropriate DAO methods:
void
commandAction(Command c, Item item) {
try {
if
(c.getLabel().equals("Next")) {
nameField.setString(dao.read());
numField.setString(dao.read());
}
else if
(c.getLabel().equals("Save")) {
dao.write(nameField.getString());
dao.write(numField.getString());
} else if (c.getLabel().equals("Del"))
{
dao.delete(); // delete name
dao.delete(); // delete phone
}
} catch (Exception e) {
String gripe = "Error:
" + e;
Alert a = new
Alert("Error", gripe, null, AlertType.ERROR);
theDisplay.setCurrent(a);
}
}
void
commandAction(Command c, Displayable d) {
if (c.getCommandType() ==
Command.EXIT) {
destroyApp(false);
notifyDestroyed();
}
}