Android 架构 3.实现
阅读原文时间:2023年07月17日阅读:1

以实现最小化可用产品(MVP)的目标,用最简单的方式来搭建架构和实现代码。
IDE采用Android Studio,Demo实现的功能为用户注册、登录和展示一个券列表,数据采用我们现有项目的测试数据,接口也是我们项目中的测试接口。

根据架构篇所讲的,将项目分为了四个层级:模型层、接口层、核心层、界面层。四个层级之间的关系如下图所示:

实现上,在Android Studio分为了相应的四个模块(Module):model、api、core、app
model为模型层,api为接口层,core为核心层,app为界面层。
model、api、core这三个模块的类型为library,app模块的类型为application。
四个模块之间的依赖设置为:model没有任何依赖,接口层依赖了模型层,核心层依赖了模型层和接口层,界面层依赖了核心层和模型层。
项目搭建的步骤如下:

创建新项目,项目名称为KAndroid,包名为com.keegan.kandroid。默认已创建了app模块,查看下app模块下的build.gradle,会看到第一行为:

apply plugin: 'com.android.application'

这行表明了app模块是application类型的。

分别新建模块model、api、core,Module Type都选为Android Library,在Add an activity to module页面选择Add No Activity,这三个模块做为库使用,并不需要界面。创建完之后,查看相应模块的build.gradle,会看到第一行为:

apply plugin: 'com.android.library'

建立模块之间的依赖关系。有两种方法可以设置:
第一种:通过右键模块,然后Open Module Settings,选择模块的Dependencies,点击左下方的加号,选择Module dependency,最后选择要依赖的模块,下图为api模块添加了model依赖;

第二种:直接在模块的build.gradle设置。打开build.gradle,在最后的dependencies一项里面添加新的一行:compile project(':ModuleName'),比如app模块添加对model模块和core模块依赖之后的dependencies如下:

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
compile project(':model')
compile project(':core')
}

通过上面两种方式的任意一种,创建了模块之间的依赖关系之后,每个模块的build.gradle的dependencies项的结果将会如下:
model:

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
}

api:

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
compile project(':model')
}

core:

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
compile project(':model')
compile project(':api')
}

app:

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
compile project(':model')
compile project(':core')
}

创建业务对象模型

业务对象模型统一存放于model模块,是对业务数据的封装,大部分都是从接口传过来的对象,因此,其属性也与接口传回的对象属性相一致。在这个Demo里,只有一个业务对象模型,封装了券的基本信息,以下是该实体类的代码:

/**
* 券的业务模型类,封装了券的基本信息。
* 券分为了三种类型:现金券、抵扣券、折扣券。
* 现金券是拥有固定面值的券,有固定的售价;
* 抵扣券是满足一定金额后可以抵扣的券,比如满100减10元;
* 折扣券是可以打折的券。
*
* @version 1.0 创建时间:15/6/21
*/
public class CouponBO implements Serializable {
private static final long serialVersionUID = -8022957276104379230L;
private int id; // 券id
private String name; // 券名称
private String introduce; // 券简介
private int modelType; // 券类型,1为现金券,2为抵扣券,3为折扣券
private double faceValue; // 现金券的面值
private double estimateAmount; // 现金券的售价
private double debitAmount; // 抵扣券的抵扣金额
private double discount; // 折扣券的折扣率(0-100)
private double miniAmount; // 抵扣券和折扣券的最小使用金额

// TODO 所有属性的getter和setter  

}

接口层的封装

在这个Demo里,提供了4个接口:一个发送验证码的接口、一个注册接口、一个登录接口、一个获取券列表的接口。这4个接口具体如下:

发送验证码接口
URL:http://uat.b.quancome.com/platform/api
参数:

参数名

描述

类型

appKey

ANDROID_KCOUPON

String

method

service.sendSmsCode4Register

String

phoneNum

手机号码

String

输出样例:

{ "event": "0", "msg": "success" }

注册接口
URL:http://uat.b.quancome.com/platform/api
参数:

参数名

描述

类型

appKey

ANDROID_KCOUPON

String

method

customer.registerByPhone

String

phoneNum

手机号码

String

code

验证码

String

password

MD5加密密码

String

输出样例:

{ "event": "0", "msg": "success" }

登录接口
URL:http://uat.b.quancome.com/platform/api
其他参数:

参数名

描述

类型

appKey

ANDROID_KCOUPON

String

method

customer.loginByApp

String

loginName

登录名(手机号)

String

password

MD5加密密码

String

imei

手机imei串号

String

loginOS

系统,android为1

int

输出样例:

{ "event": "0", "msg": "success" }

券列表
URL:http://uat.b.quancome.com/platform/api
其他参数:

参数名

描述

类型

appKey

ANDROID_KCOUPON

String

method

issue.listNewCoupon

String

currentPage

当前页数

int

pageSize

每页显示数量

int

输出样例:

{ "event": "0", "msg": "success", "maxCount": 125, "maxPage": 7, "currentPage": 1, "pageSize": 20, "objList":[
{"id": 1, "name": "测试现金券", "modelType": 1, …},
{…},

]}

在架构篇已经讲过,接口返回的json数据有三种固定结构:

{"event": "0", "msg": "success"}
{"event": "0", "msg": "success", "obj":{…}}
{"event": "0", "msg": "success", "objList":[{…}, {…}], "currentPage": 1, "pageSize": 20, "maxCount": 2, "maxPage": 1}

因此可以封装成实体类,代码如下:

public class ApiResponse {
private String event; // 返回码,0为成功
private String msg; // 返回信息
private T obj; // 单个对象
private T objList; // 数组对象
private int currentPage; // 当前页数
private int pageSize; // 每页显示数量
private int maxCount; // 总条数
private int maxPage; // 总页数

// 构造函数,初始化code和msg  
public ApiResponse(String event, String msg) {  
    this.event = event;  
    this.msg = msg;  
}

// 判断结果是否成功  
public boolean isSuccess() {  
    return event.equals("0");  
}

// TODO 所有属性的getter和setter  

}

上面4个接口,URL和appKey都是一样的,用来区别不同接口的则是method字段,因此,URL和appKey可以统一定义,method则根据不同接口定义不同常量。而除去appKey和method,剩下的参数才是每个接口需要定义的参数。因此,对上面4个接口的定义如下:

public interface Api {
// 发送验证码
public final static String SEND_SMS_CODE = "service.sendSmsCode4Register";
// 注册
public final static String REGISTER = "customer.registerByPhone";
// 登录
public final static String LOGIN = "customer.loginByApp";
// 券列表
public final static String LIST_COUPON = "issue.listNewCoupon";

/\*\*  
 \* 发送验证码  
 \*  
 \* @param phoneNum 手机号码  
 \* @return 成功时返回:{ "event": "0", "msg":"success" }  
 \*/  
public ApiResponse<Void> sendSmsCode4Register(String phoneNum);

/\*\*  
 \* 注册  
 \*  
 \* @param phoneNum 手机号码  
 \* @param code     验证码  
 \* @param password MD5加密的密码  
 \* @return 成功时返回:{ "event": "0", "msg":"success" }  
 \*/  
public ApiResponse<Void> registerByPhone(String phoneNum, String code, String password);

/\*\*  
 \* 登录  
 \*  
 \* @param loginName 登录名(手机号)  
 \* @param password  MD5加密的密码  
 \* @param imei      手机IMEI串号  
 \* @param loginOS   Android为1  
 \* @return 成功时返回:{ "event": "0", "msg":"success" }  
 \*/  
public ApiResponse<Void> loginByApp(String loginName, String password, String imei, int loginOS);

/\*\*  
 \* 券列表  
 \*  
 \* @param currentPage 当前页数  
 \* @param pageSize    每页显示数量  
 \* @return 成功时返回:{ "event": "0", "msg":"success", "objList":\[...\] }  
 \*/  
public ApiResponse<List<CouponBO>> listNewCoupon(int currentPage, int pageSize);  

}

Api的实现类则是ApiImpl了,实现类需要封装好请求数据并向服务器发起请求,并将响应结果的数据转为ApiResonse返回。而向服务器发送请求并将响应结果返回的处理则封装到http引擎类去处理。另外,这里引用了gson将json转为对象。ApiImpl的实现代码如下:

public class ApiImpl implements Api {
private final static String APP_KEY = "ANDROID_KCOUPON";
private final static String TIME_OUT_EVENT = "CONNECT_TIME_OUT";
private final static String TIME_OUT_EVENT_MSG = "连接服务器失败";
// http引擎
private HttpEngine httpEngine;

public ApiImpl() {  
    httpEngine = HttpEngine.getInstance();  
}

@Override  
public ApiResponse<Void> sendSmsCode4Register(String phoneNum) {  
    Map<String, String> paramMap = new HashMap<String, String>();  
    paramMap.put("appKey", APP\_KEY);  
    paramMap.put("method", SEND\_SMS\_CODE);  
    paramMap.put("phoneNum", phoneNum);

    Type type = new TypeToken<ApiResponse<Void>>(){}.getType();  
    try {  
        return httpEngine.postHandle(paramMap, type);  
    } catch (IOException e) {  
        return new ApiResponse(TIME\_OUT\_EVENT, TIME\_OUT\_EVENT\_MSG);  
    }  
}

@Override  
public ApiResponse<Void> registerByPhone(String phoneNum, String code, String password) {  
    Map<String, String> paramMap = new HashMap<String, String>();  
    paramMap.put("appKey", APP\_KEY);  
    paramMap.put("method", REGISTER);  
    paramMap.put("phoneNum", phoneNum);  
    paramMap.put("code", code);  
    paramMap.put("password", EncryptUtil.makeMD5(password));

    Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();  
    try {  
        return httpEngine.postHandle(paramMap, type);  
    } catch (IOException e) {  
        return new ApiResponse(TIME\_OUT\_EVENT, TIME\_OUT\_EVENT\_MSG);  
    }  
}

@Override  
public ApiResponse<Void> loginByApp(String loginName, String password, String imei, int loginOS) {  
    Map<String, String> paramMap = new HashMap<String, String>();  
    paramMap.put("appKey", APP\_KEY);  
    paramMap.put("method", LOGIN);  
    paramMap.put("loginName", loginName);  
    paramMap.put("password", EncryptUtil.makeMD5(password));  
    paramMap.put("imei", imei);  
    paramMap.put("loginOS", String.valueOf(loginOS));

    Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();  
    try {  
        return httpEngine.postHandle(paramMap, type);  
    } catch (IOException e) {  
        return new ApiResponse(TIME\_OUT\_EVENT, TIME\_OUT\_EVENT\_MSG);  
    }  
}

@Override  
public ApiResponse<List<CouponBO>> listNewCoupon(int currentPage, int pageSize) {  
    Map<String, String> paramMap = new HashMap<String, String>();  
    paramMap.put("appKey", APP\_KEY);  
    paramMap.put("method", LIST\_COUPON);  
    paramMap.put("currentPage", String.valueOf(currentPage));  
    paramMap.put("pageSize", String.valueOf(pageSize));

    Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();  
    try {  
        return httpEngine.postHandle(paramMap, type);  
    } catch (IOException e) {  
        return new ApiResponse(TIME\_OUT\_EVENT, TIME\_OUT\_EVENT\_MSG);  
    }  
}

}

而http引擎类的实现如下:

public class HttpEngine {
private final static String SERVER_URL = "http://uat.b.quancome.com/platform/api";
private final static String REQUEST_MOTHOD = "POST";
private final static String ENCODE_TYPE = "UTF-8";
private final static int TIME_OUT = 15000;

private static HttpEngine instance = null;

private HttpEngine() {  
}

public static HttpEngine getInstance() {  
    if (instance == null) {  
        instance = new HttpEngine();  
    }  
    return instance;  
}

public <T> T postHandle(Map<String, String> paramsMap, Type typeOfT) throws IOException {  
    String data = joinParams(paramsMap);  
    HttpUrlConnection connection = getConnection();  
    connection.setRequestProperty("Content-Length", String.valueOf(data.getBytes().length));  
    connection.connect();  
    OutputStream os = connection.getOutputStream();  
    os.write(data.getBytes());  
    os.flush();  
    if (connection.getResponseCode() == 200) {  
        // 获取响应的输入流对象  
        InputStream is = connection.getInputStream();  
        // 创建字节输出流对象  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        // 定义读取的长度  
        int len = 0;  
        // 定义缓冲区  
        byte buffer\[\] = new byte\[1024\];  
        // 按照缓冲区的大小,循环读取  
        while ((len = is.read(buffer)) != -1) {  
            // 根据读取的长度写入到os对象中  
            baos.write(buffer, 0, len);  
        }  
        // 释放资源  
        is.close();  
        baos.close();  
        connection.disconnect();  
        // 返回字符串  
        final String result = new String(baos.toByteArray());  
        Gson gson = new Gson();  
        return gson.fromJson(result, typeOfT);  
    } else {  
        connection.disconnect();  
        return null;  
    }  
}

private HttpURLConnection getConnection() {  
    HttpURLConnection connection = null;  
    // 初始化connection  
    try {  
        // 根据地址创建URL对象  
        URL url = new URL(SERVER\_URL);  
        // 根据URL对象打开链接  
        connection = (HttpURLConnection) url.openConnection();  
        // 设置请求的方式  
        connection.setRequestMethod(REQUEST\_MOTHOD);  
        // 发送POST请求必须设置允许输入,默认为true  
        connection.setDoInput(true);  
        // 发送POST请求必须设置允许输出  
        connection.setDoOutput(true);  
        // 设置不使用缓存  
        connection.setUseCaches(false);  
        // 设置请求的超时时间  
        connection.setReadTimeout(TIME\_OUT);  
        connection.setConnectTimeout(TIME\_OUT);  
        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");  
        connection.setRequestProperty("Connection", "keep-alive");  
        connection.setRequestProperty("Response-Type", "json");  
        connection.setChunkedStreamingMode(0);  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
    return connection;  
}

private String joinParams(Map<String, String> paramsMap) {  
    StringBuilder stringBuilder = new StringBuilder();  
    for (String key : paramsMap.keySet()) {  
        stringBuilder.append(key);  
        stringBuilder.append("=");  
        try {  
            stringBuilder.append(URLEncoder.encode(paramsMap.get(key), ENCODE\_TYPE));  
        } catch (UnsupportedEncodingException e) {  
            e.printStackTrace();  
        }  
        stringBuilder.append("&");  
    }  
    return stringBuilder.substring(0, stringBuilder.length() - 1);  
}  

}

至此,接口层的封装就完成了。接下来再往上看看核心层吧。

核心层处于接口层和界面层之间,向下调用Api,向上提供Action,它的核心任务就是处理复杂的业务逻辑。先看看我对Action的定义:

public interface AppAction {
// 发送手机验证码
public void sendSmsCode(String phoneNum, ActionCallbackListener listener);
// 注册
public void register(String phoneNum, String code, String password, ActionCallbackListener listener);
// 登录
public void login(String loginName, String password, ActionCallbackListener listener);
// 按分页获取券列表
public void listCoupon(int currentPage, ActionCallbackListener> listener);
}

首先,和Api接口对比就会发现,参数并不一致。登录并没有iemi和loginOS的参数,获取券列表的参数里也少了pageSize。这是因为,这几个参数,跟界面其实并没有直接关系。Action只要定义好跟界面相关的就可以了,其他需要的参数,在具体实现时再去获取。
另外,大部分action的处理都是异步的,因此,添加了回调监听器ActionCallbackListener,回调监听器的泛型则是返回的对象数据类型,例如获取券列表,返回的数据类型就是List,没有对象数据时则为Void。回调监听器只定义了成功和失败的方法,如下:

public interface ActionCallbackListener {
/**
* 成功时调用
*
* @param data 返回的数据
*/
public void onSuccess(T data);

/\*\*  
 \* 失败时调用  
 \*  
 \* @param errorEvemt 错误码  
 \* @param message    错误信息  
 \*/  
public void onFailure(String errorEvent, String message);  

}

接下来再看看Action的实现。首先,要获取imei,那就需要传入一个Context;另外,还需要loginOS和pageSize,这定义为常量就可以了;还有,要调用接口层,所以还需要Api实例。而接口的实现分为两步,第一步做参数检查,第二步用异步任务调用Api。具体实现如下:

public class AppActionImpl implements AppAction {
private final static int LOGIN_OS = 1; // 表示Android
private final static int PAGE_SIZE = 20; // 默认每页20条

private Context context;  
private Api api;

public AppActionImpl(Context context) {  
    this.context = context;  
    this.api = new ApiImpl();  
}

@Override  
public void sendSmsCode(final String phoneNum, final ActionCallbackListener<Void> listener) {  
    // 参数为空检查  
    if (TextUtils.isEmpty(phoneNum)) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_NULL, "手机号为空");  
        }  
        return;  
    }  
    // 参数合法性检查  
    Pattern pattern = Pattern.compile("1\\\\d{10}");  
    Matcher matcher = pattern.matcher(phoneNum);  
    if (!matcher.matches()) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_ILLEGAL, "手机号不正确");  
        }  
        return;  
    }

    // 请求Api  
    new AsyncTask<Void, Void, ApiResponse<Void>>() {  
        @Override  
        protected ApiResponse<Void> doInBackground(Void... voids) {  
            return api.sendSmsCode4Register(phoneNum);  
        }

        @Override  
        protected void onPostExecute(ApiResponse<Void> response) {  
            if (listener != null && response != null) {  
                if (response.isSuccess()) {  
                    listener.onSuccess(null);  
                } else {  
                    listener.onFailure(response.getEvent(), response.getMsg());  
                }  
            }  
        }  
    }.execute();  
}

@Override  
public void register(final String phoneNum, final String code, final String password, final ActionCallbackListener<Void> listener) {  
    // 参数为空检查  
    if (TextUtils.isEmpty(phoneNum)) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_NULL, "手机号为空");  
        }  
        return;  
    }  
    if (TextUtils.isEmpty(code)) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_NULL, "验证码为空");  
        }  
        return;  
    }  
    if (TextUtils.isEmpty(password)) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_NULL, "密码为空");  
        }  
        return;  
    }

    // 参数合法性检查  
    Pattern pattern = Pattern.compile("1\\\\d{10}");  
    Matcher matcher = pattern.matcher(phoneNum);  
    if (!matcher.matches()) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_ILLEGAL, "手机号不正确");  
        }  
        return;  
    }

    // TODO 长度检查,密码有效性检查等

    // 请求Api  
    new AsyncTask<Void, Void, ApiResponse<Void>>() {  
        @Override  
        protected ApiResponse<Void> doInBackground(Void... voids) {  
            return api.registerByPhone(phoneNum, code, password);  
        }

        @Override  
        protected void onPostExecute(ApiResponse<Void> response) {  
            if (listener != null && response != null) {  
                if (response.isSuccess()) {  
                    listener.onSuccess(null);  
                } else {  
                    listener.onFailure(response.getEvent(), response.getMsg());  
                }  
            }  
        }  
    }.execute();  
}

@Override  
public void login(final String loginName, final String password, final ActionCallbackListener<Void> listener) {  
    // 参数为空检查  
    if (TextUtils.isEmpty(loginName)) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_NULL, "登录名为空");  
        }  
        return;  
    }  
    if (TextUtils.isEmpty(password)) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_NULL, "密码为空");  
        }  
        return;  
    }

    // TODO 长度检查,密码有效性检查等        

    // 请求Api  
    new AsyncTask<Void, Void, ApiResponse<Void>>() {  
        @Override  
        protected ApiResponse<Void> doInBackground(Void... voids) {  
            TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY\_SERVICE);  
            String imei = telephonyManager.getDeviceId();  
            return api.loginByApp(loginName, password, imei, LOGIN\_OS);  
        }

        @Override  
        protected void onPostExecute(ApiResponse<Void> response) {  
            if (listener != null && response != null) {  
                if (response.isSuccess()) {  
                    listener.onSuccess(null);  
                } else {  
                    listener.onFailure(response.getEvent(), response.getMsg());  
                }  
            }  
        }  
    }.execute();  
}

@Override  
public void listCoupon(final int currentPage, final ActionCallbackListener<List<CouponBO>> listener) {  
    // 参数检查  
    if (currentPage < 0) {  
        if (listener != null) {  
            listener.onFailure(ErrorEvent.PARAM\_ILLEGAL, "当前页数小于零");  
        }  
    }

    // TODO 添加缓存

    // 请求Api  
    new AsyncTask<Void, Void, ApiResponse<List<CouponBO>>>() {  
        @Override  
        protected ApiResponse<List<CouponBO>> doInBackground(Void... voids) {  
            return api.listNewCoupon(currentPage, PAGE\_SIZE);  
        }

        @Override  
        protected void onPostExecute(ApiResponse<List<CouponBO>> response) {  
            if (listener != null && response != null) {  
                if (response.isSuccess()) {  
                    listener.onSuccess(response.getObjList());  
                } else {  
                    listener.onFailure(response.getEvent(), response.getMsg());  
                }  
            }  
        }  
    }.execute();  
}  

}

简单的实现代码就是这样,其实,这还有很多地方可以优化,比如,将参数为空的检查、手机号有效性的检查、数字型范围的检查等等,都可以抽成独立的方法,从而减少重复代码的编写。异步任务里的代码也一样,都是可以通过重构优化的。另外,需要扩展时,比如添加缓存,那就在调用Api之前处理。
核心层的逻辑就是这样了。最后就到界面层了。

界面层

在这个Demo里,只有三个页面:登录页、注册页、券列表页。在这里,也会遵循界面篇提到的三个基本原则:规范性、单一性、简洁性。
首先,界面层需要调用核心层的Action,而这会在整个应用级别都用到,因此,Action的实例最好放在Application里。代码如下:

public class KApplication extends Application {

private AppAction appAction;

@Override  
public void onCreate() {  
    super.onCreate();  
    appAction = new AppActionImpl(this);  
}

public AppAction getAppAction() {  
    return appAction;  
}  

}

另外,一个Activity的基类也是很有必要的,可以减少很多重复的工作。基类的代码如下:

public abstract class KBaseActivity extends FragmentActivity {
// 上下文实例
public Context context;
// 应用全局的实例
public KApplication application;
// 核心层的Action实例
public AppAction appAction;

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    context = getApplicationContext();  
    application = (KApplication) this.getApplication();  
    appAction = application.getAppAction();  
}  

}

再看看登录的Activity:

public class LoginActivity extends KBaseActivity {

private EditText phoneEdit;  
private EditText passwordEdit;  
private Button loginBtn;

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity\_login);  
    // 初始化View  
    initViews();  
}

@Override  
public boolean onCreateOptionsMenu(Menu menu) {  
    getMenuInflater().inflate(R.menu.menu\_login, menu);  
    return true;  
}

@Override  
public boolean onOptionsItemSelected(MenuItem item) {  
    int id = item.getItemId();

    // 如果是注册按钮  
    if (id == R.id.action\_register) {  
        Intent intent = new Intent(this, RegisterActivity.class);  
        startActivity(intent);  
        return true;  
    }

    return super.onOptionsItemSelected(item);  
}

// 初始化View  
private void initViews() {  
    phoneEdit = (EditText) findViewById(R.id.edit\_phone);  
    passwordEdit = (EditText) findViewById(R.id.edit\_password);  
    loginBtn = (Button) findViewById(R.id.btn\_login);  
}

// 准备登录  
public void toLogin(View view) {  
    String loginName = phoneEdit.getText().toString();  
    String password = passwordEdit.getText().toString();  
    loginBtn.setEnabled(false);  
    this.appAction.login(loginName, password, new ActionCallbackListener<Void>() {  
        @Override  
        public void onSuccess(Void data) {  
            Toast.makeText(context, R.string.toast\_login\_success, Toast.LENGTH\_SHORT).show();  
            Intent intent = new Intent(context, CouponListActivity.class);  
            startActivity(intent);  
            finish();  
        }

        @Override  
        public void onFailure(String errorEvent, String message) {  
            Toast.makeText(context, message, Toast.LENGTH\_SHORT).show();  
            loginBtn.setEnabled(true);  
        }  
    });  
}  

}

登录页的布局文件则如下:

<EditText  
    android:id="@+id/edit\_phone"  
    android:layout\_width="match\_parent"  
    android:layout\_height="wrap\_content"  
    android:layout\_marginTop="@dimen/edit\_vertical\_margin"  
    android:layout\_marginBottom="@dimen/edit\_vertical\_margin"  
    android:hint="@string/hint\_phone"  
    android:inputType="phone"  
    android:singleLine="true" />

<EditText  
    android:id="@+id/edit\_password"  
    android:layout\_width="match\_parent"  
    android:layout\_height="wrap\_content"  
    android:layout\_marginTop="@dimen/edit\_vertical\_margin"  
    android:layout\_marginBottom="@dimen/edit\_vertical\_margin"  
    android:hint="@string/hint\_password"  
    android:inputType="textPassword"  
    android:singleLine="true" />

<Button  
    android:id="@+id/btn\_login"  
    android:layout\_width="match\_parent"  
    android:layout\_height="wrap\_content"  
    android:layout\_marginTop="@dimen/btn\_vertical\_margin"  
    android:layout\_marginBottom="@dimen/btn\_vertical\_margin"  
    android:onClick="toLogin"  
    android:text="@string/btn\_login" />

可以看到,EditText的id命名统一以edit开头,而在Activity里的控件变量名则以Edit结尾。按钮的onClick也统一用toXXX的方式命名,明确表明这是一个将要做的动作。还有,string,dimen也都统一在相应的资源文件里按照相应的规范去定义。
注册页和登陆页差不多,这里就不展示代码了。主要再看看券列表页,因为用到了ListView,ListView需要添加适配器。实际上,适配器很多代码都是可以复用的,因此,我抽象了一个适配器的基类,代码如下:

ublic abstract class KBaseAdapter extends BaseAdapter {

protected Context context;  
protected LayoutInflater inflater;  
protected List<T> itemList = new ArrayList<T>();

public KBaseAdapter(Context context) {  
    this.context = context;  
    inflater = LayoutInflater.from(context);  
}

/\*\*  
 \* 判断数据是否为空  
 \*  
 \* @return 为空返回true,不为空返回false  
 \*/  
public boolean isEmpty() {  
    return itemList.isEmpty();  
}

/\*\*  
 \* 在原有的数据上添加新数据  
 \*  
 \* @param itemList  
 \*/  
public void addItems(List<T> itemList) {  
    this.itemList.addAll(itemList);  
    notifyDataSetChanged();  
}

/\*\*  
 \* 设置为新的数据,旧数据会被清空  
 \*  
 \* @param itemList  
 \*/  
public void setItems(List<T> itemList) {  
    this.itemList.clear();  
    this.itemList = itemList;  
    notifyDataSetChanged();  
}

/\*\*  
 \* 清空数据  
 \*/  
public void clearItems() {  
    itemList.clear();  
    notifyDataSetChanged();  
}

@Override  
public int getCount() {  
    return itemList.size();  
}

@Override  
public Object getItem(int i) {  
    return itemList.get(i);  
}

@Override  
public long getItemId(int i) {  
    return i;  
}

@Override  
abstract public View getView(int i, View view, ViewGroup viewGroup);  

}

这个抽象基类集成了设置数据的方法,每个具体的适配器类只要再实现各自的getView方法就可以了。本Demo的券列表的适配器如下:

public class CouponListAdapter extends KBaseAdapter {

public CouponListAdapter(Context context) {  
    super(context);  
}

@Override  
public View getView(int i, View view, ViewGroup viewGroup) {  
    ViewHolder holder;  
    if (view == null) {  
        view = inflater.inflate(R.layout.item\_list\_coupon, viewGroup, false);  
        holder = new ViewHolder();  
        holder.titleText = (TextView) view.findViewById(R.id.text\_item\_title);  
        holder.infoText = (TextView) view.findViewById(R.id.text\_item\_info);  
        holder.priceText = (TextView) view.findViewById(R.id.text\_item\_price);  
        view.setTag(holder);  
    } else {  
        holder = (ViewHolder) view.getTag();  
    }

    CouponBO coupon = itemList.get(i);  
    holder.titleText.setText(coupon.getName());  
    holder.infoText.setText(coupon.getIntroduce());  
    SpannableString priceString;  
    // 根据不同的券类型展示不同的价格显示方式  
    switch (coupon.getModelType()) {  
        default:  
        case CouponBO.TYPE\_CASH:  
            priceString = CouponPriceUtil.getCashPrice(context, coupon.getFaceValue(), coupon.getEstimateAmount());  
            break;  
        case CouponBO.TYPE\_DEBIT:  
            priceString = CouponPriceUtil.getVoucherPrice(context, coupon.getDebitAmount(), coupon.getMiniAmount());  
            break;  
        case CouponBO.TYPE\_DISCOUNT:  
            priceString = CouponPriceUtil.getDiscountPrice(context, coupon.getDiscount(), coupon.getMiniAmount());  
            break;  
    }  
    holder.priceText.setText(priceString);

    return view;  
}

static class ViewHolder {  
    TextView titleText;  
    TextView infoText;  
    TextView priceText;  
}

}

而券列表的Activity简单实现如下:

public class CouponListActivity extends KBaseActivity implements SwipeRefreshLayout.OnRefreshListener {
private SwipeRefreshLayout swipeRefreshLayout;
private ListView listView;
private CouponListAdapter listAdapter;
private int currentPage = 1;

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity\_coupon\_list);

    initViews();  
    getData();

    // TODO 添加上拉加载更多的功能  
}

private void initViews() {  
    swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe\_refresh\_layout);  
    swipeRefreshLayout.setOnRefreshListener(this);  
    listView = (ListView) findViewById(R.id.list\_view);  
    listAdapter = new CouponListAdapter(this);  
    listView.setAdapter(listAdapter);  
}

private void getData() {  
    this.appAction.listCoupon(currentPage, new ActionCallbackListener<List<CouponBO>>() {  
        @Override  
        public void onSuccess(List<CouponBO> data) {  
            if (!data.isEmpty()) {  
                if (currentPage == 1) { // 第一页  
                    listAdapter.setItems(data);  
                } else { // 分页数据  
                    listAdapter.addItems(data);  
                }  
            }  
            swipeRefreshLayout.setRefreshing(false);  
        }

        @Override  
        public void onFailure(String errorEvent, String message) {  
            Toast.makeText(context, message, Toast.LENGTH\_SHORT).show();  
            swipeRefreshLayout.setRefreshing(false);  
        }  
    });  
}

@Override  
public void onRefresh() {  
    // 需要重置当前页为第一页,并且清掉数据  
    currentPage = 1;  
    listAdapter.clearItems();  
    getData();  
}  

}

终于写完了,代码也终于放上了github,为了让人更容易理解,因此很多都比较简单,没有再进行扩展。
github地址:https://github.com/keeganlee/kandroid

原文:http://keeganlee.me/post/android/20150629