前言
本文源于
2005
年底一个真实的手机项目。很早就想为那个项目写点什么了,至今才提笔,也算是了却一个心愿。虽然时隔两年,但技术本身并没有发生什么太大的变化,我想本文应该能为广大开发人员提供帮助吧。
受朋友之托,他们接到一个手机应用项目(以下简称
dbMobile
)。
dbMobile
项目主要服务于零担物流运输,为广大的货主和司机建立一个畅通的交流平台,实现便利的货主找车,车主找货功能。只要货主或车主的手机支持
Java
,安装注册之后以用户身份登录上去,就能免费查询自己想要的信息。本文讲贯穿整个
dbMobile
项目,并重点介绍开发者最关注的内容。
手机端实现
(由于我是做
Java EE
应用的,为了让自己以后参考,所以关于手机端实现写得较啰嗦。)要进行
Java ME
开发,首先到
http://java.sun.com/products/sjwtoolkit/download-2_5.html
下载
WTK 2.5
,然后一步步安装好(发现安装界面比
2.2
漂亮了)。接着下载
IDE
插件,我用的开发环境是
Eclipse
,在
http://eclipseme.org/
找到
EclipseME
的安装包
eclipseme.feature_1.7.5_site
,解压缩之后(也可以不解压缩,只是安装方式稍有不同)在
Eclipse
里面新建一个
“New Local Site…”
,定位到刚才插件解压缩之后的位置,一步步安装即可。重启
Eclipse
之后可以在
“Preferences”
选项中发现
“J2ME”
菜单,现在开始配置
“WTK Root”
,如图一所示。

图一:
EclipseME
配置
1
配置好
WTK Root
之后,我们还要为
dbMobile
配置设备。如图二所示,点击
“Device Management”
,在
“Specify search directory”
中选中
WTK
根目录,然后点击右下位置的
“Refresh”
,稍等片刻,
WTK
默认的四个模拟设备就被找到了。

图二:
EclipseME
配置
2
完成了这些,如果没有特殊要求,其他选项就不用再配置了。
接着新建一个名为
dbMobile
的
J2ME
项目(既新建
“J2ME Midlet Suit”
),如果你没有安装多个
WTK
版本或者不想使用默认的彩色模拟器的的话,在新建项目的时候,无需进行过多的配置。
MIDlet
是
MIDP
的基本执行单元,如同
Servlet
继承自
javax.servlet.http.HttpServlet
一样,
MIdlet
必须继承自
javax.microedition.midlet.MIDlet
抽象类。该类定义了三个抽象方法,
startApp()
、
pauseApp()
、
destroyApp()
,应用程序管理器通过上面这三个方法控制着
MIdlet
的生命周期。在编写
MIDlet
时必须实现这三个方法。如图三所示,我为
dbMobile
创建了
HttpCli
类,该类不属于任何的包。

图三:创建
Midlet
我们来看看,类里面怎样实现抽象方法的,并以如何在启动时进入菜单画面(登录前)这个功能切入。
import
javax.microedition.lcdui.Display;
import
javax.microedition.midlet.MIDlet;
import
javax.microedition.midlet.MIDletStateChangeException;
import
com.forbidden.screen.Navigator;
/*
* MIDlet 主程序
* @author rosen jiang
* @since 2005-12
*/
public
class
HttpCli
extends
MIDlet {
/**
* 构造函数
*/
public
HttpCli() {
Navigator.midlet
=
this
;
Navigator.display
=
Display.getDisplay(
this
);
}
/**
* 启动方法
*/
public
void
startApp(){
Navigator.current
=
Navigator.MAIN_SCREEN;
Navigator.show();
}
/**
* 暂停方法
*/
protected
void
pauseApp() {
//
TODO Auto-generated method stub
}
/**
* 销毁方法
*/
protected
void
destroyApp(
boolean
arg0)
throws
MIDletStateChangeException {
this
.notifyDestroyed();
}
}
我们在构造函数
“
HttpCli()
”
中
用到了名叫
Navigator
的导航类,该类的主要作用是把
dbMobile
中所有的页面管理起来、统一进行页面跳转控制(稍后我会把
Navigator
类代码列出来)。接着看构造函数,
“Navigator.midlet = this”
的作用是把整个
MIDlet
实例交给导航类,以便在退出程序时触发。
“Navigator.display = Display.getDisplay(this)”
,在手机屏幕上显示一幅画面就是一个
Display
对象要实现的功能,从
MIDlet
实例中获取
Display
对象实例,也就是在向导航类授予一个进行画面切换的控制权。接着看
“startApp
()
”
启动方法,同样调用了导航类,并设置启动后首先进入的页面是菜单画面(登录前)。
接下来我们看看
Navigator
导航类都有些什么。
package
com.forbidden.screen;
import
javax.microedition.midlet.MIDlet;
import
javax.microedition.lcdui.
*
;
/*
* 导航类
* @author rosen jiang
* @since 2005-12
*/
public
class
Navigator{
//
菜单画面(登录前)
final
public
static
int
MAIN_SCREEN
=
1
;
//
用户注册
final
public
static
int
USER_REG
=
2
;
//
车主找货
final
public
static
int
AUTO_FIND_GOODS
=
3
;
//
用户登录
final
public
static
int
USER_LOGIN
=
4
;
//
菜单画面(登录后)
final
public
static
int
MENU_SCREEN
=
5
;
//
货主找车
final
public
static
int
GOODS_FIND_AUTO
=
6
;
//
空车信息发布
final
public
static
int
AUTO_PUB
=
7
;
//
货物信息发布
final
public
static
int
GOODS_PUB
=
8
;
//
注册信息更新
final
public
static
int
REG_UPD
=
9
;
public
static
MIDlet midlet;
public
static
Display display;
//
当前位置
public
static
int
current;
/**
* 转向要显示的菜单
*/
public
static
void
show (){
switch
(current){
case
MAIN_SCREEN:
display.setCurrent(MainScreen.getInstance());
break
;
case
USER_REG:
display.setCurrent(UserReg.getInstance());
break
;
case
AUTO_FIND_GOODS:
display.setCurrent(AutoFindGoods.getInstance());
break
;
case
USER_LOGIN:
display.setCurrent(LoginScreen.getInstance());
break
;
case
MENU_SCREEN:
display.setCurrent(MenuScreen.getInstance(
null
));
break
;
case
GOODS_FIND_AUTO:
display.setCurrent(GoodsFindAuto.getInstance());
break
;
case
AUTO_PUB:
display.setCurrent(AutoPub.getInstance());
break
;
case
GOODS_PUB:
display.setCurrent(GoodsPub.getInstance());
break
;
case
REG_UPD:
display.setCurrent(RegUpd.getInstance(
null
,
null
,
null
,
null
));
break
;
}
}
/**
* 导航器定位目标表单
*
*
@param
String cmd 输入的命令
*/
public
static
void
flow(String cmd){
if
(cmd.equals(
"
离开
"
)){
midlet.notifyDestroyed();
}
else
if
(cmd.equals(
"
注册
"
)){
current
=
USER_REG;
show ();
}
else
if
(cmd.equals(
"
车主找货
"
)){
current
=
AUTO_FIND_GOODS;
show ();
}
else
if
(cmd.equals(
"
登陆
"
)){
current
=
USER_LOGIN;
show ();
}
else
if
(cmd.equals(
"
功能列表
"
)){
current
=
MENU_SCREEN;
show ();
}
else
if
(cmd.equals(
"
返回菜单
"
)){
current
=
MAIN_SCREEN;
show ();
}
else
if
(cmd.equals(
"
货主找车
"
)){
current
=
GOODS_FIND_AUTO;
show ();
}
else
if
(cmd.equals(
"
空车信息发布
"
)){
current
=
AUTO_PUB;
show ();
}
else
if
(cmd.equals(
"
货物信息发布
"
)){
current
=
GOODS_PUB;
show ();
}
else
if
(cmd.equals(
"
修改注册信息
"
)){
current
=
REG_UPD;
show ();
}
}
}
该类对每个画面进行了编号处理,
“show()”
方法是整个导航类的关键,当符合条件的画面编号被找到时,调用
“display.setCurrent()”
方法设置被显示画面的实例,同时手机上也会切换到相应画面。
“flow()”
方法做用是捕获用户的控制命令,并把命令转换成内部的画面编号,和
“show()”
联合使用就能响应用户操作了。
下面是菜单画面(登录前)类。
package
com.forbidden.screen;
import
javax.microedition.lcdui.
*
;
/*
* 菜单画面(登录前)
* @author rosen jiang
* @since 2005-12
*/
public
class
MainScreen
extends
List
implements
CommandListener{
//
对象实例
private
static
Displayable instance;
/**
* 获取对象实例
*/
synchronized
public
static
Displayable getInstance(){
if
(instance
==
null
)
instance
=
new
MainScreen();
return
instance;
}
/**
* 画面内容
*/
private
MainScreen(){
super
(
"
菜单
"
, Choice.IMPLICIT);
append (
"
注册
"
,
null
);
append (
"
登陆
"
,
null
);
addCommand(
new
Command(
"
进入
"
,Command.OK,
1
));
addCommand(
new
Command(
"
离开
"
,Command.EXIT,
1
));
setCommandListener(
this
);
}
/**
* 对用户输入命令作出反应
*
@param
c 命令
*
@param
s Displayable 对象
*/
public
void
commandAction(Command c, Displayable s){
String cmd
=
c.getLabel();
if
(cmd.equals(
"
进入
"
)){
String comd
=
getString(getSelectedIndex());
Navigator.flow(comd);
}
else
if
(cmd.equals(
"
离开
"
)) {
Navigator.flow(cmd);
}
}
}

图四:高级界面类图
Displayable 是所有高级(Screan)、低级(Canvas)界面的父类,在 dbMobile 项目中,由于专注于数据而不是界面,所以我决定采用高级界面。图四列出了高级界面的类、接口关系,可以对整个高级界面开发有个概括。关于高级界面编程基础的话题就不多说了,请参考其他资料。
在整个程序加载的时候会首先实例化HttpCli类,接着触发”Navigator.MAIN_SCREEN”,最后实例化MainScreen类,在手机屏幕上显示如图五的画面。MainScreen类的“getInstance()”方法返回 MainScreen 唯一对象实例。在“MainScreen()”构造函数中,“super ("菜单", Choice.IMPLICIT)”创建名为“菜单”的单选列表,然后分别用“append("注册",null)”和“append ("登陆",null)”追加两个选项,接着追加两个命令“addCommand(new Command("进入",Command.OK,1))”和“addCommand(new Command("离开",Command.EXIT,1))”,最后针对当前对象实例设置命令监听器“setCommandListener(this)”。在手机上一切都已正确显示后就可以监听用户的操作了,“commandAction()”方法捕捉用户点击的是”离开”还是”进入”。如果是”离开”,直接利用Navigator类退出整个程序,如果是”进入”则通过”String comd = getString(getSelectedIndex())”代码获取用户选择的菜单,然后再通过Navigator类的”flow()”方法实例化相应的画面实例,就像进入菜单画面(登录前)一样。

图五:主菜单(登录前)
可能你非常熟悉以上这些调用流程,本文到这里开始转到如何与 Java EE服务器端通讯的部分。

图六:主菜单(登录后)
登录成功以后进入主菜单(登录后)如图六所示,现在我重点介绍货主找车这个功能,首先要创建货主找车界面,GoodsFindAuto类代码如下:
package com.forbidden.screen;
import java.util.Date;
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.DateField;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.TextField;
import com.forbidden.thread.GoodsFindAutoThread;
import com.forbidden.vo.TransAuto;
/* 货主找车输入查询条件页面
* @author rosen jiang
* @since 2005-12
*/
public class GoodsFindAuto extends Form implements CommandListener {
//车辆出发地
private TextField autoFromField;
//车辆目的地
private TextField autoTargetField;
//发布时间
private DateField pubDateField;
//对象实例
private static Displayable instance;
/**
* 获取对象实例
*/
synchronized public static Displayable getInstance(){
if (instance==null)
instance = new GoodsFindAuto("货主找车");
return instance;
}
/**
* 画面内容
*/
public GoodsFindAuto(String arg0) {
super(arg0);
autoFromField = new TextField("车辆出发地", "28", 25, TextField.NUMERIC);
autoTargetField = new TextField("车辆目的地", null, 25, TextField.NUMERIC);
pubDateField = new DateField("发布日期", DateField.DATE);
pubDateField.setDate(new Date());
append(autoFromField);
append(autoTargetField);
append(pubDateField);
Command backCommand = new Command("功能列表", Command.BACK, 1);
Command sendCommand = new Command("查询", Command.SCREEN, 1);
addCommand(backCommand);
addCommand(sendCommand);
setCommandListener(this);
}
/**
* 对用户输入命令作出反应
* @param c 命令
* @param s Displayable 对象
*/
public void commandAction(Command c, Displayable s) {
String cmd = c.getLabel();
if (cmd.equals("查询")){
String autoFrom = autoFromField.getString();
String autoTarget = autoTargetField.getString();
if (autoTarget.length()==0) {
Alert a = new Alert("提示信息", "目的城市不能为空!", null, AlertType.ERROR);
a.setTimeout(Alert.FOREVER);
Navigator.display.setCurrent(a);
return;
}
String pubDate = pubDateField.getDate().getTime()+"";
//发送查询
TransAuto ta = new TransAuto(null,null,null,null,
pubDate,autoFrom,autoTarget, null);
GoodsFindAutoThread gfat = new GoodsFindAutoThread(1,20,ta);
Navigator.display.setCurrent(WaitForm.getInstance());
gfat.start();
}else{
Navigator.flow(cmd);
}
}
}
对于手机用户来说,要用最简单的界面实现查询功能那是最好不过了。在构造函数里面添加了三个输入框"车辆出发地"、"车辆目的地"、”发布日期”,为了更进一步减少用户输入,在"车辆出发地"和”车辆目的地"是按照当地的去掉0的电话区号来作为条件,默认的以成都(28)为车辆出发地,运行效果如图七所示。

图七:货主找车
当用户完成查询并点击”查询之后”,要对用户的输入信息进行判断,根据业务上的要求,”车辆目的地”是必填项,如果为空,在”commandAction()”方法中会通过Alert对象进行提示。接下来将与服务器进行数据交互,交互之前先把查询条件构造成TransAuto车辆对象实例并进行序列化,然后再通过HTTP GET方法请求服务器,服务器收到序列化的数据后抽取查询条件。手机端和服务器端通讯的策略是:从手机端到服务器端是通过拼接字符串然后GET过去,而从服务器端到手机端则通过UTF-8编码后的数据流送回来,否则容易出现乱码。如果你要问为什么不使用GBK、GB2312编码输出,我的回答是DataOutputStream/ DataInputStream类原生支持”writeUTF()/readUTF()”方法,无论是在服务器端还是手机端,转换起来很轻松,尽管UTF-8三字节编码会产生更多的通讯流量。”GoodsFindAutoThread(1,20,ta)”构造函数来自GoodsFindAutoThread线程类,该线程类用于远程HTTP连接,由于GPRS连接非常慢,为了提高网络利用率,要一次多传些查询结果到手机端,这就涉及到了分页,我定义的分页策略是:一次从服务器端取最多20