|
前言 本文源于 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; } } |