|
前言
本文源于 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” 。
配置好 WTK Root 之后,我们还要为 dbMobile 配置设备。点击 “Device Management” ,在 “Specify search directory” 中选中 WTK 根目录,然后点击右下位置的 “Refresh” ,稍等片刻, WTK 默认的四个模拟设备就被找到了。
完成了这些,如果没有特殊要求,其他选项就不用再配置了。
接着新建一个名为 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 类,该类不属于任何的包。
我们来看看,类里面怎样实现抽象方法的,并以如何在启动时进入菜单画面(登录前)这个功能切入。
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条记录,然后在手机上分成4页显示(每页5条);如果总记录数超过20条,当手机将要阅读第5页的时候再取下20条。那么上面的构造函数实际上是发出了获取从1—4页共20条数据的分页请求。在进入线程类的话题之前,先看看TransAuto车辆类。
package com.forbidden.vo;
import java.io.DataInputStream;
import java.io.IOException;
import com.forbidden.util.Split;
/* 车辆
* @author rosen jiang
* @since 2005-12
*/
public class TransAuto{
//车主名
private String name;
//车牌号
private String autoNo;
//联系电话
private String phone;
//车辆容积
private String autoCap;
//发布时间
private String pubDate;
//车辆出发地
private String autoFrom;
//车辆目的地
private String autoTarget;
//备注
private String memo;
/**
* 构造函数
*
* @param name 车主名
* @param autoNo 车牌号
* @param phone 联系电话
* @param autoCap 车辆容积
* @param pubDate 发布时间
* @param autoFrom 车辆出发地
* @param autoTarget 车辆目的地
* @param memo 备注
*/
public TransAuto(String name, String autoNo,
String phone,String autoCap, String pubDate,
String autoFrom,String autoTarget,String memo) {
this.name=name;
this.autoNo=autoNo;
this.phone=phone;
this.autoCap=autoCap;
this.pubDate=pubDate;
this.autoFrom=autoFrom;
this.autoTarget=autoTarget;
this.memo=memo;
}
/**
* 序列化
*
* @return 字符串
*/
public String serialize() {
String outStrings = "pubDate="+pubDate+"&autoFrom="
+autoFrom+"&autoTarget="+autoTarget;
return outStrings;
}
/**
* 多对象的反序列化
*
* @param from 车辆出发地
* @param rows 条数
* @param din 输入流
* @return TransAuto[] 车辆数组
* @throws IOException
*/
public TransAuto[] deserializes(String from,int rows,DataInputStream din) throws IOException {
TransAuto[] tas = new TransAuto[rows];
for (int i = 0; i < rows; i++) {
String recString = null;
try{
recString = din.readUTF();
if(recString.equals("")){
break;
}
}catch(Exception e){
break;
}
String[] recStrings = Split.split(recString,"&");
try{
name = recStrings[0];
autoNo = recStrings[1];
phone = recStrings[2];
autoCap = recStrings[3];
pubDate = recStrings[4]+"时";
autoFrom = from;
autoTarget = recStrings[5];
memo = recStrings[6];
}catch(ArrayIndexOutOfBoundsException e){
break;
}
TransAuto ta = new TransAuto(name,autoNo,phone,
autoCap,pubDate,autoFrom,autoTarget,memo);
tas[i]=ta;
}
return tas;
}
public String getAutoCap() {
return autoCap;
}
public void setAutoCap(String autoCap) {
this.autoCap = autoCap;
}
public String getAutoFrom() {
return autoFrom;
}
public void setAutoFrom(String autoFrom) {
this.autoFrom = autoFrom;
}
public String getAutoNo() {
return autoNo;
}
public void setAutoNo(String autoNo) {
this.autoNo = autoNo;
}
public String getAutoTarget() {
return autoTarget;
}
public void setAutoTarget(String autoTarget) {
this.autoTarget = autoTarget;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMemo() {
return memo;
}
public void setMemo(String memo) {
this.memo = memo;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getPubDate() {
return pubDate;
}
public void setPubDate(String pubDate) {
this.pubDate = pubDate;
}
}
|