成都心情@北漂

本 Blog 是从:http://blog.csdn.net/rosen 搬来。

  BlogJava :: 首页 ::  ::  :: 聚合  :: 管理 ::
  74 随笔 :: 2 文章 :: 348 评论 :: 1 Trackbacks

前言

 

本文源于 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” ,如图一所示。

                                                dbMobile1.jpg

                                    图一: EclipseME 配置 1

 

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


               dbMobile2.jpg

                             图二: 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 类,该类不属于任何的包。

                              dbMobile3.jpg

                                   图三:创建 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);
        }
    }
}

                                                    dbMobile4.jpg

                    图四:高级界面类图

 

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()”方法实例化相应的画面实例,就像进入菜单画面(登录前)一样。



                                            dbMobile5.jpg
                                              图五:主菜单(登录前)

 

可能你非常熟悉以上这些调用流程,本文到这里开始转到如何与 Java EE服务器端通讯的部分。


                                                                dbMobile10.jpg

                  图六:主菜单(登录后)

 

登录成功以后进入主菜单(登录后)如图六所示,现在我重点介绍货主找车这个功能,首先要创建货主找车界面,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("车辆目的地"null25, 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)为车辆出发地,运行效果如图七所示。

                                                                            dbMobile6.jpg

                    图七:货主找车

 

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