by Jonathan Knudsen
January 2002
Download:
[source code]
[MIDlet suite JAD]
[MIDlet suite JAR]
The user's experience of an application is important to its success. A
powerful application with a poor user interface will not be
popular, whether it runs on desktop computers or small devices, but the
demands are
more stringent in the small device world. Desktop users, hardened by years of
experience, may tolerate interfaces that are somewhat clumsy. Small devices,
however, fall into the category of consumer electronics, where user expectations
are much higher.
Many MIDlets make connections to the network. Because today's wireless
networks are slow, a user interface question arises: what kind of
feedback does the user see? As you'll see, answering this question
will lead into a discussion of multithreading.
This article describes several approaches to providing user feedback during
network activity, and to managing threads appropriately. It starts with a simple
MIDlet that provides no feedback,
then gradually enhance it.
A Stone Age Networked MIDlet
When I was in junior high school, my shop class was treated to a movie about
a cartoon antihero who used tools inappropriately. He was
forever banging on nails
with a ratchet wrench or trying to cut sheet metal with a screwdriver. If
this character were a programmer, he might write a networked MIDlet like this:
import java.io.*;
import javax.microedition.io.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class PrimitiveMIDlet
extends MIDlet
implements CommandListener {
private Display mDisplay;
private Form mMainForm;
private Command mExitCommand, mConnectCommand;
public void startApp() {
mDisplay = Display.getDisplay(this);
if (mMainForm == null) {
mMainForm = new Form("PrimitiveMIDlet");
mMainForm.append(new StringItem("",
"Select Connect to make a network connection"));
mExitCommand = new Command("Exit", Command.EXIT, 0);
mConnectCommand = new Command("Connect", Command.SCREEN, 0);
mMainForm.addCommand(mExitCommand);
mMainForm.addCommand(mConnectCommand);
mMainForm.setCommandListener(this);
}
mDisplay.setCurrent(mMainForm);
}
public void commandAction(Command c, Displayable s) {
if (c == mExitCommand)
notifyDestroyed();
else if (c == mConnectCommand)
connect();
}
private void connect() {
String url = getAppProperty("NetworkThreading.URL");
try {
// Query the server and retrieve the response.
HttpConnection hc = (HttpConnection)Connector.open(url);
InputStream in = hc.openInputStream();
// Pull back the server's response. If a content length is not
// specified, we'll just read 255 bytes.
int contentLength = (int)hc.getLength();
if (contentLength == -1) contentLength = 255;
byte[] raw = new byte[contentLength];
int length = in.read(raw);
// Clean up.
in.close();
hc.close();
// Show the response to the user.
String s = new String(raw, 0, length);
Alert a = new Alert("Response", s, null, null);
a.setTimeout(Alert.FOREVER);
mDisplay.setCurrent(a, mMainForm);
}
catch (IOException ioe) {
Alert a = new Alert("Exception", ioe.toString(), null, null);
a.setTimeout(Alert.FOREVER);
mDisplay.setCurrent(a, mMainForm);
}
}
public void pauseApp() {}
public void destroyApp(boolean unconditional) {}
}
|
PrimitiveMIDlet's screen flow is simple. It
presents a main screen. From there, you can either exit the application or make
a network connection. If you choose the network connection, the results from the
server appear as shown.
Screen flow for PrimitiveMIDlet
|
What's so primitive about this code?
-
PrimitiveMIDlet doesn't provide any visual feedback while it's
connecting to the
network. This is not a problem
in a development environment, where a device emulator is connecting
to some server running on the programmer's local machine.
The connection takes
a fraction of a second, hardly perceptible. In the real world, the
delay will be much more noticable. The user will be staring at an
apparently frozen application for five or ten seconds while the MIDlet connects
to the server.
-
This Neanderthal programmer has hijacked a system thread for his own lengthy
processing. The system
calls his
commandAction() method when the user selects a
command. The thread that calls this method belongs to the system, not to
the developer. This would not be a crime if the method executed swiftly,
but in this case, the network
connection may hog the system's thread for a long time.
In J2SE
application programming and even in servlet programming, the system
creates a thread for you and there are few restrictions on how long your
processing can take.
The rule of MIDlet threading is simple and strict: the only threads that belong
to you are the ones you create yourself.
In a MIDlet, you are writing code that the system will call from one of
its own threads.
When your MIDlet's
startApp(), pauseApp(), destroyApp(),
and event handler methods are called, for example, they run inside a
system thread. Your methods must return quickly so that the
system thread can continue its other work. Any task that can't be completed
quickly must be moved out of the system's thread.
This style of programming may take some getting used
to, as you are really only
writing code that is called from the system's threads. If you've done
any other
GUI programming, however, this technique will be familiar. AWT and Swing have an
event dispatch thread that handles operating system events and calls
event handlers in your code. The rule is the same: event handlers should execute
quickly and return control to the event dispatch thread so that the rest of the
interface doesn't lock up.
The diagram below shows how the system thread calls methods in a MIDlet. The
left side, in yellow, represents code in the underlying implementation
of MIDP, while
the right side, in blue, represents the code you have written. Time
increases going down. The red line shows typical activity for the system's event
thread. It spends most of its time in system code, harvesting events from the
underlying device, repainting windows, and performing other important work. It
makes brief excursions into your application code to call MIDlet life cycle
methods or event handler methods.
Normal operation of the system event thread
|
The next diagram reveals the flaw in PrimitiveMIDlet.
The system thread gets
hung up in the MIDlet's connect() method. While the system thread
is waiting for a network connection, it is unable to fulfill its other
obligations. The user interface freezes, the device appears to be dead, and
the user becomes frustrated.
Hijacking the system event thread
|
Using a New Thread for Network Activity
The way out of this trouble is to create a new thread that will
perform the network connection. Instead of calling connect()
in the system thread, the commandAction() method will
create a separate
thread that will call connect(), then terminate.
The only code that needs to change is
in commandAction().
public void commandAction(Command c, Displayable s) {
if (c == mExitCommand)
notifyDestroyed();
else if (c == mConnectCommand) {
Thread t = new Thread() {
public void run() {
connect();
}
};
t.start();
}
}
|
The MIDlet is now properly threaded. The following diagram shows how the new
thread (in green) handles the network connection, leaving the system event
thread (in red) to go about its business. If you
download the source code
for this article, you'll find this example is ThreadedMIDlet.java.
Using a new thread for network activity
|
Unfortunately, the new thread introduces a new wrinkle. Because network activity
takes place in the new thread, the user interface remains responsive--a good
thing generally, but a problem here:
an impatient user could invoke the Connect command repeatedly, creating a
new thread each time:
Too many threads
|
In addition, we still haven't addressed a fundamental question: what does the
user see when the application is connecting to the network? Adding a simple
wait screen will answer that question, and solve the multiple thread
problem, and observer a key precept of good user interface programming:
Let the User Know What's Happening
Clearly, you need to make the user wait while the MIDlet performs its
network connection. Displaying a wait screen does the job simply and
intuitively. It
informs the user that the MIDlet is busy and also prevents the user from banging
on the Connect command and spewing out more threads.
The simplest tactic is to create a new screen. We'll use a form with
only a title. We can initialize it in startApp() at the same time
we initialize our main screen.
mWaitForm = new Form("Waiting...");
|
Then it's a simple matter for commandAction() to display the wait
screen just before it starts the network connection thread. In the
downloaded code,
this example is
SimpleWaitMIDlet.java.
public void commandAction(Command c, Displayable s) {
if (c == mExitCommand)
notifyDestroyed();
else if (c == mConnectCommand) {
mDisplay.setCurrent(mWaitForm);
Thread t = new Thread() {
public void run() {
connect();
}
};
t.start();
}
}
|
The screen flow for SimpleWaitMIDlet now includes a third screen:
Screen flow for SimpleWaitMIDlet
|
As long as the wait screen is displayed, the user cannot invoke the
Connect command, so the MIDlet will never generate more than
one network thread at a time. At
the same time, the wait screen reassures the user that the application is
still working. SimpleWaitMIDlet is about midway between rudimentary
and deluxe. Read on to learn how to improve on this simple design.
Allow the User to Cancel
To improve on SimpleWaitMIDlet, let's begin by cleaning up the
code. The MIDlet class serves several purposes and the threading is
confusing. We'll begin by moving the connect() method into
a separate class, CleanWorker. We'll have this worker class call
back methods in the MIDlet class. A more elaborate implementation might define a
listener interface for the worker class, but for now we'll keep things
simple. The next code sample shows the new MIDlet class,
CleanMIDlet.
At the same time we're tidying up the code, let's add a crucial element to the
user interface. Because wireless networks tend to be slow and
unreliable, we should give impatient users the option to cancel a connection
in progress.
To augment the user interface, we'll just add a Command to the wait
form. To abort the connection, we'll close() the current
HttpConnection.
CleanMIDlet handles the creation of the command
and stores a reference to it in a member variable named
mCancelCommand. The startApp() method also
registers the MIDlet as the listener for command events from the wait form. Don't
get confused here; the MIDlet is the command listener for both the main form
and the wait form. In practice, the result is that the event handling code for the
Cancel command is a simple addition to the MIDlet's
commandAction() method. If the user does invoke the Cancel
command, we just set the current screen back to the main form and call the
worker class's
cancel() method.
import java.io.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class CleanMIDlet
extends MIDlet
implements CommandListener {
private Display mDisplay;
private Form mMainForm, mWaitForm;
private Command mExitCommand, mConnectCommand, mCancelCommand;
private CleanWorker mWorker;
public void startApp() {
mDisplay = Display.getDisplay(this);
if (mMainForm == null) {
mMainForm = new Form("CleanMIDlet");
mMainForm.append(new StringItem("",
"Select Connect to make a network connection"));
mExitCommand = new Command("Exit", Command.EXIT, 0);
mConnectCommand = new Command("Connect", Command.SCREEN, 0);
mMainForm.addCommand(mExitCommand);
mMainForm.addCommand(mConnectCommand);
mMainForm.setCommandListener(this);
mWaitForm = new Form("Waiting...");
mCancelCommand = new Command("Cancel", Command.CANCEL, 0);
mWaitForm.addCommand(mCancelCommand);
mWaitForm.setCommandListener(this);
}
mDisplay.setCurrent(mMainForm);
}
public void commandAction(Command c, Displayable s) {
if (c == mExitCommand)
notifyDestroyed();
else if (c == mConnectCommand) {
mDisplay.setCurrent(mWaitForm);
String url = getAppProperty("NetworkThreading.URL");
mWorker = new CleanWorker(this, url);
mWorker.start();
}
else if (c == mCancelCommand) {
mDisplay.setCurrent(mMainForm);
mWorker.cancel();
mWorker = null;
}
}
public void pauseApp() {}
public void destroyApp(boolean unconditional) {}
public void networkResponse(String s) {
Alert a = new Alert("Response", s, null, null);
a.setTimeout(Alert.FOREVER);
mDisplay.setCurrent(a, mMainForm);
}
public void networkException(IOException ioe) {
Alert a = new Alert("Exception", ioe.toString(), null, null);
a.setTimeout(Alert.FOREVER);
mDisplay.setCurrent(a, mMainForm);
}
}
|
The connect() method doesn't change much when it moves into
the network worker class. The worker is a subclass of Thread,
which makes it easy to arrange that it runs
in its own thread.
When the cancel() method is called, it sets a boolean
member variable, mCancel, and closes
the currently open connection, mHttpConnection.
Closing the connection forces an IOException
in the connect() method, whose exception handler
checks
mCancel to see whether the exception resulted
from the user canceling the connection.
Note in connect() the call to setRequestProperty(),
which turns
off HTTP persistent connections. By default, the client may reuse outgoing
connections to the same server. This feature does not work correctly with the
J2MEWTK 1.0.3
emulator when connections are interrupted. Passing a
Connection: close header in the HTTP request turns the
feature off.
import java.io.*;
import javax.microedition.io.*;
public class CleanWorker extends Thread {
private CleanMIDlet mMIDlet;
private String mURL;
private HttpConnection mHttpConnection;
private boolean mCancel;
public CleanWorker(CleanMIDlet midlet, String url) {
mMIDlet = midlet;
mURL = url;
mCancel = false;
}
public void run() {
connect();
}
public void cancel() {
try {
mCancel = true;
if (mHttpConnection != null)
mHttpConnection.close();
}
catch (IOException ignored) {}
}
private void connect() {
InputStream in = null;
try {
// Query the server and retrieve the response.
mHttpConnection = (HttpConnection)Connector.open(mURL);
mHttpConnection.setRequestProperty("Connection", "close");
in = mHttpConnection.openInputStream();
// Pull back the server's response. If a content length is not
// specified, we'll just read 255 bytes.
int contentLength = (int)mHttpConnection.getLength();
if (contentLength == -1) contentLength = 255;
byte[] raw = new byte[contentLength];
int length = in.read(raw);
// Clean up.
in.close();
mHttpConnection.close();
// Show the response to the user.
String s = new String(raw, 0, length);
mMIDlet.networkResponse(s);
}
catch (IOException ioe) {
if (mCancel == false) {
try {
if (in != null) in.close();
if (mHttpConnection != null) mHttpConnection.close();
}
catch (IOException ignored) {}
mMIDlet.networkException(ioe);
}
mCancel = false;
}
}
}
|
Using One Thread for Network Activity
We've resolved our threading troubles and have some relatively clean code,
but we're not handling threading as efficiently as we could. Currently, we
create a new worker thread is created every time the user invokes the
Connect command:
Clean threading
|
On the small platforms that run MIDP, creating a new thread consumes
a lot of processing power and memory. It would be nice if we could reuse a
single thread to do all the network stuff instead of creating a new thread every
time we wanted to hit the network. The following diagram illustrates this
technique. The worker thread is always present, but it becomes active
(bold) only when network activity is requested.
Using a single worker thread
|
Now that network activity is split into a separate worker class, this change is
not too difficult. We'll create a new SingleWorker class based on
our CleanWorker class. The run() method, instead of
calling connect() once, now loops. It sits quietly in
wait(). When another thread uses notify() to
tell it to wake up, it calls the connect() method. The excerpt
below shows the updated run() method as well as the
go() method that wakes up the thread to do its network stuff. The
mTrucking member variable is a boolean that indicates whether the
SingleWorker instance is currently running, or should terminate.
public synchronized void run() {
while (mTrucking) {
try { wait(); }
catch (InterruptedException ie) {}
if (mTrucking) connect();
}
}
public synchronized void go() {
notify();
}
|
The corresponding MIDlet, SingleMIDlet, is little changed from
CleanMIDlet. Its startApp() method creates a
SingleWorker instance and kicks it off by calling
start(). Whenever the application needs a network connection,
commandAction() just calls SingleWorker's
go() method.
import java.io.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class SingleMIDlet
extends MIDlet
implements CommandListener {
private Display mDisplay;
private Form mMainForm, mWaitForm;
private SingleWorker mWorker;
private Command mExitCommand, mConnectCommand, mCancelCommand;
public void startApp() {
mDisplay = Display.getDisplay(this);
if (mMainForm == null) {
mMainForm = new Form("SingleMIDlet");
mMainForm.append(new StringItem("",
"Select Connect to make a network connection"));
mExitCommand = new Command("Exit", Command.EXIT, 0);
mConnectCommand = new Command("Connect", Command.SCREEN, 0);
mMainForm.addCommand(mExitCommand);
mMainForm.addCommand(mConnectCommand);
mMainForm.setCommandListener(this);
mWaitForm = new Form("Waiting...");
mCancelCommand = new Command("Cancel", Command.CANCEL, 0);
mWaitForm.addCommand(mCancelCommand);
mWaitForm.setCommandListener(this);
String url = getAppProperty("NetworkThreading.URL");
mWorker = new SingleWorker(this, url);
mWorker.start();
}
mDisplay.setCurrent(mMainForm);
}
public void commandAction(Command c, Displayable s) {
if (c == mExitCommand)
notifyDestroyed();
else if (c == mConnectCommand) {
mDisplay.setCurrent(mWaitForm);
mWorker.go();
}
else if (c == mCancelCommand) {
mDisplay.setCurrent(mMainForm);
mWorker.cancel();
}
}
public void pauseApp() {}
public void destroyApp(boolean unconditional) {
mWorker.stop();
}
public void networkResponse(String s) {
Alert a = new Alert("Response", s, null, null);
a.setTimeout(Alert.FOREVER);
mDisplay.setCurrent(a, mMainForm);
}
public void networkException(IOException ioe) {
Alert a = new Alert("Exception", ioe.toString(), null, null);
a.setTimeout(Alert.FOREVER);
mDisplay.setCurrent(a, mMainForm);
}
}
|
Make It Pretty
Now that the code looks better, let's make a prettier wait screen. Our
wait screen will be implemented as a Canvas subclass called
WaitCanvas. It displays a rotating wheel and features a message
that can be updated dynamically. To animate the wheel we use yet another thread
(a Timer, really) that is initialized in WaitCanvas's
constructor. Our thread diagram now includes a pink thread
for animation of the wait screen.
Worker and animation threads
|
WaitCanvas is listed below. Most of its code concerns
drawing, but note the creation of a Timer and
TimerTask in the constructor.
import java.util.*;
import javax.microedition.lcdui.*;
public class WaitCanvas
extends Canvas {
private int mCount, mMaximum;
private int mInterval;
private int mWidth, mHeight, mX, mY, mRadius;
private String mMessage;
public WaitCanvas() {
mCount = 0;
mMaximum = 36;
mInterval = 100;
mWidth = getWidth();
mHeight = getHeight();
// Calculate the radius.
int halfWidth = (mWidth - mRadius) / 2;
int halfHeight = (mHeight - mRadius) / 2;
mRadius = Math.min(halfWidth, halfHeight);
// Calculate the location.
mX = halfWidth - mRadius / 2;
mY = halfHeight - mRadius / 2;
// Create a Timer to update the display.
TimerTask task = new TimerTask() {
public void run() {
mCount = (mCount + 1) % mMaximum;
repaint();
}
};
Timer timer = new Timer();
timer.schedule(task, 0, mInterval);
}
public void setMessage(String s) {
mMessage = s;
repaint();
}
public void paint(Graphics g) {
int theta = -(mCount * 360 / mMaximum);
// Clear the whole screen.
g.setColor(255, 255, 255);
g.fillRect(0, 0, mWidth, mHeight);
// Now draw the pinwheel.
g.setColor(0, 0, 0);
g.drawArc(mX, mY, mRadius, mRadius, 0, 360);
g.fillArc(mX, mY, mRadius, mRadius, theta + 90, 90);
g.fillArc(mX, mY, mRadius, mRadius, theta + 270, 90);
// Draw the message, if there is a message.
if (mMessage != null)
g.drawString(mMessage, mWidth / 2, mHeight,
Graphics.BOTTOM | Graphics.HCENTER);
}
}
|
The new MIDlet and worker classes, DeluxeMIDlet and
DeluxeWorker, are similar to their predecessors. Note, though,
that DeluxeWorker needs explicit knowledge of the wait
screen in order to update the message that's displayed as the network
connection is made. A reference to the wait screen is passed to
DeluxeWorker's constructor and saved in a member variable.
The screen flow is the same as for SimpleWaitMIDlet, but the
waiting screen is much more attractive, as shown below.
Screen flow for DeluxeMIDlet
|
Summary
This article presented six different approaches to network threading and user
experience. To help you find your way through the sample code, here's a summary
of all six examples.
-
PrimitiveMIDlet demonstrated a bad way to do network access. It
used the system's event dispatch thread to perform a lengthy operation.
-
ThreadedMIDlet fixed one threading problem and created another. It
performed
network activity in a new thread, but it allowed the user
to hammer on the Connect
command, spawning multiple threads, and it provided no feedback about the
network activity.
-
SimpleWaitMIDlet used a form to display a wait screen, which
both informed the user
of the network activity
and limited the number of network threads to one.
-
The same source code was broken into two pieces,
CleanMIDlet and
CleanWorker, for clarity and maintainability. At the same time,
we added the
capability to cancel a connection.
-
SingleMIDlet and SingleWorker reused a single thread
for all network activity
instead of creating a new thread for each network connection.
-
Finally,
DeluxeMIDlet, DeluxeWorker, and
WaitCanvas demonstrated an aesthetically pleasing implementation of
a wait screen.
You can download the source code as a
ZIP file.
Alternately, you can download the
JAD file
and
JAR file
for a MIDlet suite that contains all of the examples.
About the Author: Jonathan Knudsen
[e-mail]
[home page]
is the author of several books,
including
Wireless Java (second edition),
The Unofficial Guide to LEGO MINDSTORMS Robots,
Learning Java (second edition), and
Java 2D Graphics.
Jonathan has written
extensively about Java and Lego robots,
including articles for JavaWorld, EXE, NZZ Folio,
and the O'Reilly Network.
Jonathan
holds a degree in mechanical engineering from Princeton University.
Back To Top
|
|