J2ME (Part II)

Stack Calculator

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.

Implementation

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

Building the Form

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

Executing Commands

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 Calculator Bean

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.

Model-View-Control Pattern

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.

Persistence and Record Stores

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:

Implementation

Here are the necessary includes:

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.rms.*;
import java.io.*;

Data Access Objects

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() { ... }
}

Writing Strings

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

Reading Strings

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

Deleting Strings

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

Closing the Store

   void close() {
      try {
         rs.closeRecordStore();
         // rs.deleteRecordStore(rsName);
      } catch (Exception e) {
         System.err.println("---> " + e);
      }
   }

AddressBook.java

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) {... }
}

Building the Form

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

Handling Commands

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();
      }
   }