前两篇文章Android项目重构之路:架构篇Android项目重构之路:界面篇已经讲了我的项目开始搭建时的架构设计和界面设计,这篇就讲讲具体怎么实现的,以实现最小化可用产品(MVP)的目标,用最简单的方式来搭建架构和实现代码。 IDE采用Android Studio,Demo实现的功能为用户注册、登录和展示一个券列表,数据采用我们现有项目的测试数据,接口也是我们项目中的测试接口。

项目搭建

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

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

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

    1. apply plugin: 'com.android.application'

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

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

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

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

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

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

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

    api:

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

    core:

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

    app:

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

创建业务对象模型

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

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

接口层的封装

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

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

    参数名 描述 类型
    appKey ANDROID_KCOUPON String
    method service.sendSmsCode4Register String
    phoneNum 手机号码 String

    输出样例:

    1. { "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

    输出样例:

    1. { "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

    输出样例:

    1. { "event": "0", "msg": "success" }
  • 券列表 URL:http://uat.b.quancome.com/platform/api 其他参数:

    参数名 描述 类型
    appKey ANDROID_KCOUPON String
    method issue.listNewCoupon String
    currentPage 当前页数 int
    pageSize 每页显示数量 int

    输出样例:

    1. { "event": "0", "msg": "success", "maxCount": 125, "maxPage": 7, "currentPage": 1, "pageSize": 20, "objList":[
    2. {"id": 1, "name": "测试现金券", "modelType": 1, ...},
    3. {...},
    4. ...
    5. ]}

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

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

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

  1. public class ApiResponse<T> {
  2. private String event; // 返回码,0为成功
  3. private String msg; // 返回信息
  4. private T obj; // 单个对象
  5. private T objList; // 数组对象
  6. private int currentPage; // 当前页数
  7. private int pageSize; // 每页显示数量
  8. private int maxCount; // 总条数
  9. private int maxPage; // 总页数
  10.  
  11. // 构造函数,初始化code和msg
  12. public ApiResponse(String event, String msg) {
  13. this.event = event;
  14. this.msg = msg;
  15. }
  16.  
  17. // 判断结果是否成功
  18. public boolean isSuccess() {
  19. return event.equals("0");
  20. }
  21.  
  22. // TODO 所有属性的getter和setter
  23. }

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

  1. public interface Api {
  2. // 发送验证码
  3. public final static String SEND_SMS_CODE = "service.sendSmsCode4Register";
  4. // 注册
  5. public final static String REGISTER = "customer.registerByPhone";
  6. // 登录
  7. public final static String LOGIN = "customer.loginByApp";
  8. // 券列表
  9. public final static String LIST_COUPON = "issue.listNewCoupon";
  10.  
  11. /**
  12. * 发送验证码
  13. *
  14. * @param phoneNum 手机号码
  15. * @return 成功时返回:{ "event": "0", "msg":"success" }
  16. */
  17. public ApiResponse<Void> sendSmsCode4Register(String phoneNum);
  18.  
  19. /**
  20. * 注册
  21. *
  22. * @param phoneNum 手机号码
  23. * @param code 验证码
  24. * @param password MD5加密的密码
  25. * @return 成功时返回:{ "event": "0", "msg":"success" }
  26. */
  27. public ApiResponse<Void> registerByPhone(String phoneNum, String code, String password);
  28.  
  29. /**
  30. * 登录
  31. *
  32. * @param loginName 登录名(手机号)
  33. * @param password MD5加密的密码
  34. * @param imei 手机IMEI串号
  35. * @param loginOS Android为1
  36. * @return 成功时返回:{ "event": "0", "msg":"success" }
  37. */
  38. public ApiResponse<Void> loginByApp(String loginName, String password, String imei, int loginOS);
  39.  
  40. /**
  41. * 券列表
  42. *
  43. * @param currentPage 当前页数
  44. * @param pageSize 每页显示数量
  45. * @return 成功时返回:{ "event": "0", "msg":"success", "objList":[...] }
  46. */
  47. public ApiResponse<List<CouponBO>> listNewCoupon(int currentPage, int pageSize);
  48. }

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

  1. public class ApiImpl implements Api {
  2. private final static String APP_KEY = "ANDROID_KCOUPON";
  3. private final static String TIME_OUT_EVENT = "CONNECT_TIME_OUT";
  4. private final static String TIME_OUT_EVENT_MSG = "连接服务器失败";
  5. // http引擎
  6. private HttpEngine httpEngine;
  7.  
  8. public ApiImpl() {
  9. httpEngine = HttpEngine.getInstance();
  10. }
  11.  
  12. @Override
  13. public ApiResponse<Void> sendSmsCode4Register(String phoneNum) {
  14. Map<String, String> paramMap = new HashMap<String, String>();
  15. paramMap.put("appKey", APP_KEY);
  16. paramMap.put("method", SEND_SMS_CODE);
  17. paramMap.put("phoneNum", phoneNum);
  18.  
  19. Type type = new TypeToken<ApiResponse<Void>>(){}.getType();
  20. try {
  21. return httpEngine.postHandle(paramMap, type);
  22. } catch (IOException e) {
  23. return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG);
  24. }
  25. }
  26.  
  27. @Override
  28. public ApiResponse<Void> registerByPhone(String phoneNum, String code, String password) {
  29. Map<String, String> paramMap = new HashMap<String, String>();
  30. paramMap.put("appKey", APP_KEY);
  31. paramMap.put("method", REGISTER);
  32. paramMap.put("phoneNum", phoneNum);
  33. paramMap.put("code", code);
  34. paramMap.put("password", EncryptUtil.makeMD5(password));
  35.  
  36. Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();
  37. try {
  38. return httpEngine.postHandle(paramMap, type);
  39. } catch (IOException e) {
  40. return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG);
  41. }
  42. }
  43.  
  44. @Override
  45. public ApiResponse<Void> loginByApp(String loginName, String password, String imei, int loginOS) {
  46. Map<String, String> paramMap = new HashMap<String, String>();
  47. paramMap.put("appKey", APP_KEY);
  48. paramMap.put("method", LOGIN);
  49. paramMap.put("loginName", loginName);
  50. paramMap.put("password", EncryptUtil.makeMD5(password));
  51. paramMap.put("imei", imei);
  52. paramMap.put("loginOS", String.valueOf(loginOS));
  53.  
  54. Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();
  55. try {
  56. return httpEngine.postHandle(paramMap, type);
  57. } catch (IOException e) {
  58. return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG);
  59. }
  60. }
  61.  
  62. @Override
  63. public ApiResponse<List<CouponBO>> listNewCoupon(int currentPage, int pageSize) {
  64. Map<String, String> paramMap = new HashMap<String, String>();
  65. paramMap.put("appKey", APP_KEY);
  66. paramMap.put("method", LIST_COUPON);
  67. paramMap.put("currentPage", String.valueOf(currentPage));
  68. paramMap.put("pageSize", String.valueOf(pageSize));
  69.  
  70. Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();
  71. try {
  72. return httpEngine.postHandle(paramMap, type);
  73. } catch (IOException e) {
  74. return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG);
  75. }
  76. }
  77.  
  78. }

而http引擎类的实现如下:

  1. public class HttpEngine {
  2. private final static String SERVER_URL = "http://uat.b.quancome.com/platform/api";
  3. private final static String REQUEST_MOTHOD = "POST";
  4. private final static String ENCODE_TYPE = "UTF-8";
  5. private final static int TIME_OUT = 15000;
  6.  
  7. private static HttpEngine instance = null;
  8.  
  9. private HttpEngine() {
  10. }
  11.  
  12. public static HttpEngine getInstance() {
  13. if (instance == null) {
  14. instance = new HttpEngine();
  15. }
  16. return instance;
  17. }
  18.  
  19. public <T> T postHandle(Map<String, String> paramsMap, Type typeOfT) throws IOException {
  20. String data = joinParams(paramsMap);
  21. HttpUrlConnection connection = getConnection();
  22. connection.setRequestProperty("Content-Length", String.valueOf(data.getBytes().length));
  23. connection.connect();
  24. OutputStream os = connection.getOutputStream();
  25. os.write(data.getBytes());
  26. os.flush();
  27. if (connection.getResponseCode() == 200) {
  28. // 获取响应的输入流对象
  29. InputStream is = connection.getInputStream();
  30. // 创建字节输出流对象
  31. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  32. // 定义读取的长度
  33. int len = 0;
  34. // 定义缓冲区
  35. byte buffer[] = new byte[1024];
  36. // 按照缓冲区的大小,循环读取
  37. while ((len = is.read(buffer)) != -1) {
  38. // 根据读取的长度写入到os对象中
  39. baos.write(buffer, 0, len);
  40. }
  41. // 释放资源
  42. is.close();
  43. baos.close();
  44. connection.disconnect();
  45. // 返回字符串
  46. final String result = new String(baos.toByteArray());
  47. Gson gson = new Gson();
  48. return gson.fromJson(result, typeOfT);
  49. } else {
  50. connection.disconnect();
  51. return null;
  52. }
  53. }
  54.  
  55. private HttpURLConnection getConnection() {
  56. HttpURLConnection connection = null;
  57. // 初始化connection
  58. try {
  59. // 根据地址创建URL对象
  60. URL url = new URL(SERVER_URL);
  61. // 根据URL对象打开链接
  62. connection = (HttpURLConnection) url.openConnection();
  63. // 设置请求的方式
  64. connection.setRequestMethod(REQUEST_MOTHOD);
  65. // 发送POST请求必须设置允许输入,默认为true
  66. connection.setDoInput(true);
  67. // 发送POST请求必须设置允许输出
  68. connection.setDoOutput(true);
  69. // 设置不使用缓存
  70. connection.setUseCaches(false);
  71. // 设置请求的超时时间
  72. connection.setReadTimeout(TIME_OUT);
  73. connection.setConnectTimeout(TIME_OUT);
  74. connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
  75. connection.setRequestProperty("Connection", "keep-alive");
  76. connection.setRequestProperty("Response-Type", "json");
  77. connection.setChunkedStreamingMode(0);
  78. } catch (IOException e) {
  79. e.printStackTrace();
  80. }
  81. return connection;
  82. }
  83.  
  84. private String joinParams(Map<String, String> paramsMap) {
  85. StringBuilder stringBuilder = new StringBuilder();
  86. for (String key : paramsMap.keySet()) {
  87. stringBuilder.append(key);
  88. stringBuilder.append("=");
  89. try {
  90. stringBuilder.append(URLEncoder.encode(paramsMap.get(key), ENCODE_TYPE));
  91. } catch (UnsupportedEncodingException e) {
  92. e.printStackTrace();
  93. }
  94. stringBuilder.append("&");
  95. }
  96. return stringBuilder.substring(0, stringBuilder.length() - 1);
  97. }
  98. }

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

核心层的逻辑

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

  1. public interface AppAction {
  2. // 发送手机验证码
  3. public void sendSmsCode(String phoneNum, ActionCallbackListener<Void> listener);
  4. // 注册
  5. public void register(String phoneNum, String code, String password, ActionCallbackListener<Void> listener);
  6. // 登录
  7. public void login(String loginName, String password, ActionCallbackListener<Void> listener);
  8. // 按分页获取券列表
  9. public void listCoupon(int currentPage, ActionCallbackListener<List<CouponBO>> listener);
  10. }

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

  1. public interface ActionCallbackListener<T> {
  2. /**
  3. * 成功时调用
  4. *
  5. * @param data 返回的数据
  6. */
  7. public void onSuccess(T data);
  8.  
  9. /**
  10. * 失败时调用
  11. *
  12. * @param errorEvemt 错误码
  13. * @param message 错误信息
  14. */
  15. public void onFailure(String errorEvent, String message);
  16. }

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

  1. public class AppActionImpl implements AppAction {
  2. private final static int LOGIN_OS = 1; // 表示Android
  3. private final static int PAGE_SIZE = 20; // 默认每页20条
  4.  
  5. private Context context;
  6. private Api api;
  7.  
  8. public AppActionImpl(Context context) {
  9. this.context = context;
  10. this.api = new ApiImpl();
  11. }
  12.  
  13. @Override
  14. public void sendSmsCode(final String phoneNum, final ActionCallbackListener<Void> listener) {
  15. // 参数为空检查
  16. if (TextUtils.isEmpty(phoneNum)) {
  17. if (listener != null) {
  18. listener.onFailure(ErrorEvent.PARAM_NULL, "手机号为空");
  19. }
  20. return;
  21. }
  22. // 参数合法性检查
  23. Pattern pattern = Pattern.compile("1\\d{10}");
  24. Matcher matcher = pattern.matcher(phoneNum);
  25. if (!matcher.matches()) {
  26. if (listener != null) {
  27. listener.onFailure(ErrorEvent.PARAM_ILLEGAL, "手机号不正确");
  28. }
  29. return;
  30. }
  31.  
  32. // 请求Api
  33. new AsyncTask<Void, Void, ApiResponse<Void>>() {
  34. @Override
  35. protected ApiResponse<Void> doInBackground(Void... voids) {
  36. return api.sendSmsCode4Register(phoneNum);
  37. }
  38.  
  39. @Override
  40. protected void onPostExecute(ApiResponse<Void> response) {
  41. if (listener != null && response != null) {
  42. if (response.isSuccess()) {
  43. listener.onSuccess(null);
  44. } else {
  45. listener.onFailure(response.getEvent(), response.getMsg());
  46. }
  47. }
  48. }
  49. }.execute();
  50. }
  51.  
  52. @Override
  53. public void register(final String phoneNum, final String code, final String password, final ActionCallbackListener<Void> listener) {
  54. // 参数为空检查
  55. if (TextUtils.isEmpty(phoneNum)) {
  56. if (listener != null) {
  57. listener.onFailure(ErrorEvent.PARAM_NULL, "手机号为空");
  58. }
  59. return;
  60. }
  61. if (TextUtils.isEmpty(code)) {
  62. if (listener != null) {
  63. listener.onFailure(ErrorEvent.PARAM_NULL, "验证码为空");
  64. }
  65. return;
  66. }
  67. if (TextUtils.isEmpty(password)) {
  68. if (listener != null) {
  69. listener.onFailure(ErrorEvent.PARAM_NULL, "密码为空");
  70. }
  71. return;
  72. }
  73.  
  74. // 参数合法性检查
  75. Pattern pattern = Pattern.compile("1\\d{10}");
  76. Matcher matcher = pattern.matcher(phoneNum);
  77. if (!matcher.matches()) {
  78. if (listener != null) {
  79. listener.onFailure(ErrorEvent.PARAM_ILLEGAL, "手机号不正确");
  80. }
  81. return;
  82. }
  83.  
  84. // TODO 长度检查,密码有效性检查等
  85.  
  86. // 请求Api
  87. new AsyncTask<Void, Void, ApiResponse<Void>>() {
  88. @Override
  89. protected ApiResponse<Void> doInBackground(Void... voids) {
  90. return api.registerByPhone(phoneNum, code, password);
  91. }
  92.  
  93. @Override
  94. protected void onPostExecute(ApiResponse<Void> response) {
  95. if (listener != null && response != null) {
  96. if (response.isSuccess()) {
  97. listener.onSuccess(null);
  98. } else {
  99. listener.onFailure(response.getEvent(), response.getMsg());
  100. }
  101. }
  102. }
  103. }.execute();
  104. }
  105.  
  106. @Override
  107. public void login(final String loginName, final String password, final ActionCallbackListener<Void> listener) {
  108. // 参数为空检查
  109. if (TextUtils.isEmpty(loginName)) {
  110. if (listener != null) {
  111. listener.onFailure(ErrorEvent.PARAM_NULL, "登录名为空");
  112. }
  113. return;
  114. }
  115. if (TextUtils.isEmpty(password)) {
  116. if (listener != null) {
  117. listener.onFailure(ErrorEvent.PARAM_NULL, "密码为空");
  118. }
  119. return;
  120. }
  121.  
  122. // TODO 长度检查,密码有效性检查等
  123.  
  124. // 请求Api
  125. new AsyncTask<Void, Void, ApiResponse<Void>>() {
  126. @Override
  127. protected ApiResponse<Void> doInBackground(Void... voids) {
  128. TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
  129. String imei = telephonyManager.getDeviceId();
  130. return api.loginByApp(loginName, password, imei, LOGIN_OS);
  131. }
  132.  
  133. @Override
  134. protected void onPostExecute(ApiResponse<Void> response) {
  135. if (listener != null && response != null) {
  136. if (response.isSuccess()) {
  137. listener.onSuccess(null);
  138. } else {
  139. listener.onFailure(response.getEvent(), response.getMsg());
  140. }
  141. }
  142. }
  143. }.execute();
  144. }
  145.  
  146. @Override
  147. public void listCoupon(final int currentPage, final ActionCallbackListener<List<CouponBO>> listener) {
  148. // 参数检查
  149. if (currentPage < 0) {
  150. if (listener != null) {
  151. listener.onFailure(ErrorEvent.PARAM_ILLEGAL, "当前页数小于零");
  152. }
  153. }
  154.  
  155. // TODO 添加缓存
  156.  
  157. // 请求Api
  158. new AsyncTask<Void, Void, ApiResponse<List<CouponBO>>>() {
  159. @Override
  160. protected ApiResponse<List<CouponBO>> doInBackground(Void... voids) {
  161. return api.listNewCoupon(currentPage, PAGE_SIZE);
  162. }
  163.  
  164. @Override
  165. protected void onPostExecute(ApiResponse<List<CouponBO>> response) {
  166. if (listener != null && response != null) {
  167. if (response.isSuccess()) {
  168. listener.onSuccess(response.getObjList());
  169. } else {
  170. listener.onFailure(response.getEvent(), response.getMsg());
  171. }
  172. }
  173. }
  174. }.execute();
  175. }
  176. }

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

界面层

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

  1. public class KApplication extends Application {
  2.  
  3. private AppAction appAction;
  4.  
  5. @Override
  6. public void onCreate() {
  7. super.onCreate();
  8. appAction = new AppActionImpl(this);
  9. }
  10.  
  11. public AppAction getAppAction() {
  12. return appAction;
  13. }
  14. }

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

  1. public abstract class KBaseActivity extends FragmentActivity {
  2. // 上下文实例
  3. public Context context;
  4. // 应用全局的实例
  5. public KApplication application;
  6. // 核心层的Action实例
  7. public AppAction appAction;
  8.  
  9. @Override
  10. protected void onCreate(Bundle savedInstanceState) {
  11. super.onCreate(savedInstanceState);
  12. context = getApplicationContext();
  13. application = (KApplication) this.getApplication();
  14. appAction = application.getAppAction();
  15. }
  16. }

再看看登录的Activity:

  1. public class LoginActivity extends KBaseActivity {
  2.  
  3. private EditText phoneEdit;
  4. private EditText passwordEdit;
  5. private Button loginBtn;
  6.  
  7. @Override
  8. protected void onCreate(Bundle savedInstanceState) {
  9. super.onCreate(savedInstanceState);
  10. setContentView(R.layout.activity_login);
  11. // 初始化View
  12. initViews();
  13. }
  14.  
  15. @Override
  16. public boolean onCreateOptionsMenu(Menu menu) {
  17. getMenuInflater().inflate(R.menu.menu_login, menu);
  18. return true;
  19. }
  20.  
  21. @Override
  22. public boolean onOptionsItemSelected(MenuItem item) {
  23. int id = item.getItemId();
  24.  
  25. // 如果是注册按钮
  26. if (id == R.id.action_register) {
  27. Intent intent = new Intent(this, RegisterActivity.class);
  28. startActivity(intent);
  29. return true;
  30. }
  31.  
  32. return super.onOptionsItemSelected(item);
  33. }
  34.  
  35. // 初始化View
  36. private void initViews() {
  37. phoneEdit = (EditText) findViewById(R.id.edit_phone);
  38. passwordEdit = (EditText) findViewById(R.id.edit_password);
  39. loginBtn = (Button) findViewById(R.id.btn_login);
  40. }
  41.  
  42. // 准备登录
  43. public void toLogin(View view) {
  44. String loginName = phoneEdit.getText().toString();
  45. String password = passwordEdit.getText().toString();
  46. loginBtn.setEnabled(false);
  47. this.appAction.login(loginName, password, new ActionCallbackListener<Void>() {
  48. @Override
  49. public void onSuccess(Void data) {
  50. Toast.makeText(context, R.string.toast_login_success, Toast.LENGTH_SHORT).show();
  51. Intent intent = new Intent(context, CouponListActivity.class);
  52. startActivity(intent);
  53. finish();
  54. }
  55.  
  56. @Override
  57. public void onFailure(String errorEvent, String message) {
  58. Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
  59. loginBtn.setEnabled(true);
  60. }
  61. });
  62. }
  63. }

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

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical"
  6. android:paddingBottom="@dimen/activity_vertical_margin"
  7. android:paddingLeft="@dimen/activity_horizontal_margin"
  8. android:paddingRight="@dimen/activity_horizontal_margin"
  9. android:paddingTop="@dimen/activity_vertical_margin"
  10. tools:context="com.keegan.kandroid.activity.LoginActivity">
  11.  
  12. <EditText
  13. android:id="@+id/edit_phone"
  14. android:layout_width="match_parent"
  15. android:layout_height="wrap_content"
  16. android:layout_marginTop="@dimen/edit_vertical_margin"
  17. android:layout_marginBottom="@dimen/edit_vertical_margin"
  18. android:hint="@string/hint_phone"
  19. android:inputType="phone"
  20. android:singleLine="true" />
  21.  
  22. <EditText
  23. android:id="@+id/edit_password"
  24. android:layout_width="match_parent"
  25. android:layout_height="wrap_content"
  26. android:layout_marginTop="@dimen/edit_vertical_margin"
  27. android:layout_marginBottom="@dimen/edit_vertical_margin"
  28. android:hint="@string/hint_password"
  29. android:inputType="textPassword"
  30. android:singleLine="true" />
  31.  
  32. <Button
  33. android:id="@+id/btn_login"
  34. android:layout_width="match_parent"
  35. android:layout_height="wrap_content"
  36. android:layout_marginTop="@dimen/btn_vertical_margin"
  37. android:layout_marginBottom="@dimen/btn_vertical_margin"
  38. android:onClick="toLogin"
  39. android:text="@string/btn_login" />
  40.  
  41. </LinearLayout>

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

  1. public abstract class KBaseAdapter<T> extends BaseAdapter {
  2.  
  3. protected Context context;
  4. protected LayoutInflater inflater;
  5. protected List<T> itemList = new ArrayList<T>();
  6.  
  7. public KBaseAdapter(Context context) {
  8. this.context = context;
  9. inflater = LayoutInflater.from(context);
  10. }
  11.  
  12. /**
  13. * 判断数据是否为空
  14. *
  15. * @return 为空返回true,不为空返回false
  16. */
  17. public boolean isEmpty() {
  18. return itemList.isEmpty();
  19. }
  20.  
  21. /**
  22. * 在原有的数据上添加新数据
  23. *
  24. * @param itemList
  25. */
  26. public void addItems(List<T> itemList) {
  27. this.itemList.addAll(itemList);
  28. notifyDataSetChanged();
  29. }
  30.  
  31. /**
  32. * 设置为新的数据,旧数据会被清空
  33. *
  34. * @param itemList
  35. */
  36. public void setItems(List<T> itemList) {
  37. this.itemList.clear();
  38. this.itemList = itemList;
  39. notifyDataSetChanged();
  40. }
  41.  
  42. /**
  43. * 清空数据
  44. */
  45. public void clearItems() {
  46. itemList.clear();
  47. notifyDataSetChanged();
  48. }
  49.  
  50. @Override
  51. public int getCount() {
  52. return itemList.size();
  53. }
  54.  
  55. @Override
  56. public Object getItem(int i) {
  57. return itemList.get(i);
  58. }
  59.  
  60. @Override
  61. public long getItemId(int i) {
  62. return i;
  63. }
  64.  
  65. @Override
  66. abstract public View getView(int i, View view, ViewGroup viewGroup);
  67. }

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

  1. public class CouponListAdapter extends KBaseAdapter<CouponBO> {
  2.  
  3. public CouponListAdapter(Context context) {
  4. super(context);
  5. }
  6.  
  7. @Override
  8. public View getView(int i, View view, ViewGroup viewGroup) {
  9. ViewHolder holder;
  10. if (view == null) {
  11. view = inflater.inflate(R.layout.item_list_coupon, viewGroup, false);
  12. holder = new ViewHolder();
  13. holder.titleText = (TextView) view.findViewById(R.id.text_item_title);
  14. holder.infoText = (TextView) view.findViewById(R.id.text_item_info);
  15. holder.priceText = (TextView) view.findViewById(R.id.text_item_price);
  16. view.setTag(holder);
  17. } else {
  18. holder = (ViewHolder) view.getTag();
  19. }
  20.  
  21. CouponBO coupon = itemList.get(i);
  22. holder.titleText.setText(coupon.getName());
  23. holder.infoText.setText(coupon.getIntroduce());
  24. SpannableString priceString;
  25. // 根据不同的券类型展示不同的价格显示方式
  26. switch (coupon.getModelType()) {
  27. default:
  28. case CouponBO.TYPE_CASH:
  29. priceString = CouponPriceUtil.getCashPrice(context, coupon.getFaceValue(), coupon.getEstimateAmount());
  30. break;
  31. case CouponBO.TYPE_DEBIT:
  32. priceString = CouponPriceUtil.getVoucherPrice(context, coupon.getDebitAmount(), coupon.getMiniAmount());
  33. break;
  34. case CouponBO.TYPE_DISCOUNT:
  35. priceString = CouponPriceUtil.getDiscountPrice(context, coupon.getDiscount(), coupon.getMiniAmount());
  36. break;
  37. }
  38. holder.priceText.setText(priceString);
  39.  
  40. return view;
  41. }
  42.  
  43. static class ViewHolder {
  44. TextView titleText;
  45. TextView infoText;
  46. TextView priceText;
  47. }
  48.  
  49. }

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

  1. public class CouponListActivity extends KBaseActivity implements SwipeRefreshLayout.OnRefreshListener {
  2. private SwipeRefreshLayout swipeRefreshLayout;
  3. private ListView listView;
  4. private CouponListAdapter listAdapter;
  5. private int currentPage = 1;
  6.  
  7. @Override
  8. protected void onCreate(Bundle savedInstanceState) {
  9. super.onCreate(savedInstanceState);
  10. setContentView(R.layout.activity_coupon_list);
  11.  
  12. initViews();
  13. getData();
  14.  
  15. // TODO 添加上拉加载更多的功能
  16. }
  17.  
  18. private void initViews() {
  19. swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
  20. swipeRefreshLayout.setOnRefreshListener(this);
  21. listView = (ListView) findViewById(R.id.list_view);
  22. listAdapter = new CouponListAdapter(this);
  23. listView.setAdapter(listAdapter);
  24. }
  25.  
  26. private void getData() {
  27. this.appAction.listCoupon(currentPage, new ActionCallbackListener<List<CouponBO>>() {
  28. @Override
  29. public void onSuccess(List<CouponBO> data) {
  30. if (!data.isEmpty()) {
  31. if (currentPage == 1) { // 第一页
  32. listAdapter.setItems(data);
  33. } else { // 分页数据
  34. listAdapter.addItems(data);
  35. }
  36. }
  37. swipeRefreshLayout.setRefreshing(false);
  38. }
  39.  
  40. @Override
  41. public void onFailure(String errorEvent, String message) {
  42. Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
  43. swipeRefreshLayout.setRefreshing(false);
  44. }
  45. });
  46. }
  47.  
  48. @Override
  49. public void onRefresh() {
  50. // 需要重置当前页为第一页,并且清掉数据
  51. currentPage = 1;
  52. listAdapter.clearItems();
  53. getData();
  54. }
  55. }

完结

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

(转)Android项目重构之路:实现篇的更多相关文章

  1. (转)Android项目重构之路:界面篇

    在前一篇文章<Android项目重构之路:架构篇>中已经简单说明了项目的架构,将项目分为了四个层级:模型层.接口层.核心层.界面层.其中,最上层的界面,是变化最频繁的一个层面,也是最复杂最 ...

  2. (转)Android项目重构之路:架构篇

    去年10月底换到了新公司,做移动研发组的负责人,刚开始接手android项目时,发现该项目真的是一团糟.首先是其架构,是按功能模块进行划分的,本来按模块划分也挺好的,可是,他却分得太细,总共分为了17 ...

  3. Android Studio重构之路,我们重新来了解一下Google官方的Android开发工具

    Android Studio重构之路,我们重新来了解一下Google官方的Android开发工具 记得我的第一篇博客就是写Android Studio,但是现在看来还是有些粗糙了,所有重构了一下思路, ...

  4. Jenkins构建Android项目持续集成之findbugs的使用

    Findbugs简介 关于findbugs的介绍,可以自行百度下,这里贴下百度百科的介绍.findbugs是一个静态分析工具,它检查类或者 JAR 文件,将字节码与一组缺陷模式进行对比以发现可能的问题 ...

  5. Android项目刮刮奖详解扩展篇——开源刮刮奖View的制作

    Android项目刮刮奖详解(四) 前言 我们已经成功实现了刮刮奖的功能了,本期是扩展篇,我们把这个View直接定义成开源控件,发布到JitPack上,以后有需要也可以直接使用,关于自定义控件的知识, ...

  6. Android项目开发遇到的问题(64K的错误)的解决之路,从入坑到出坑

    自己一个android项目,一直以来进展还算顺利,没有遇到什么严重性的问题,今天准备给同事手机上安装一下玩玩,谁知丢人丢大,无法build apk!报错!my god,我开发没问题啊,我手机连上usb ...

  7. Eclipse开发Android项目报错解决方案详细教程,最新版一篇就够了!

    本文记录刚接触Android开发搭建环境后新建工程各种可能的报错,并亲身经历漫长的解决过程(╥╯^╰╥),寻找各种偏方,避免大家采坑,希望能帮助到大家. 报错信息 出错一:The import and ...

  8. Hybrid框架UI重构之路:五、前端那点事儿(HTML、CSS)

    上文回顾 :Hybird框架UI重构之路:四.分而治之 这里讲述在开发的过程中,一些HTML.CSS的关键点. 单页模式的页面结构 在单页模式中,弱化HTML的概念,把HTML当成一个容器,BODY中 ...

  9. Hybrid框架UI重构之路:六、前端那点事儿(Javascript)

    上文回顾 :Hybird框架UI重构之路:五.前端那点事儿(HTML.CSS) 这里讲述在开发的过程中,一些JS的关键点. 换肤 对于终端的换肤,我之前一篇文章有说了我的想法. 请查看:http:// ...

随机推荐

  1. EL表达式使用时出现NumberFormatException异常

    从后端数据库取出书本集合,然后循环输出到前端表格: <c:forEach items="${bookManagedBean.bookList}" var="book ...

  2. IOS中div contenteditable=true无法输入 fastclick.js在点击一个可输入的div时,ios无法正常唤起输入法键盘

    原文地址: https://blog.csdn.net/u010377383/article/details/79838562 前言 为了提升移动端click的响应速度,使用了fastclick.js ...

  3. hdu多校第三场

    Problem D. Euler Function 思路:打表找找规律. #include<bits/stdc++.h> #define LL long long #define fi f ...

  4. 一款你不容错过的Laravel后台管理扩展包 —— Voyager

    http://laravelacademy.org/post/6401.html  Posted on 2016年11月1日 by  学院君 1.简介 Voyager是一个你不容错过的Laravel后 ...

  5. NBUT 1220 SPY

    $map$,简单模拟. #include<cstdio> #include<cstring> #include<cmath> #include<algorit ...

  6. 【最短路径】 SPFA算法

    上一期介绍到了SPFA算法,只是一笔带过,这一期让我们详细的介绍一下SPFA. 1 SPFA原理介绍 SPFA算法和dijkstra算法特别像,总感觉自己讲的不行,同学说我的博客很辣鸡,推荐一个视频讲 ...

  7. Git Bash 将本地代码提交到Github

    前提:已拥有Token,并且把本地的Token配置到了自己的Github里面(没有Token的自行去百度如何配置Token) 测试一下自己的连接 ssh -T git@github.com 本地操作: ...

  8. Python开发基础-Day4-布尔运算、集合

    布尔值 True 真 False 假 所有的数据类型都自带布尔值,数据只有在0,None和空的时候为False. print(bool()) print(bool()) print(bool('')) ...

  9. ARP扫描工具arp-scan

    ARP扫描工具arp-scan   arp-scan是Kali Linux自带的一款ARP扫描工具.该工具可以进行单一目标扫描,也可以进行批量扫描.批量扫描的时候,用户可以通过CIDR.地址范围或者列 ...

  10. 通过myEclipse创建hibernate的实体类

    今天有个新项目中需要使用到hibernate,刚好数据库表已经创建完毕,就顺便来总结一下通过myEclipse创建hibernate的实体类. 1..在myEclipse中选择MyEclipse Da ...