Android專案重構之路:架構篇
Android專案重構之路:介面篇
Android專案重構之路:實現篇
前兩篇文章《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沒有任何依賴,介面層依賴了模型層,核心層依賴了模型層和介面層,介面層依賴了核心層和模型層。
專案搭建的步驟如下:
- 建立新專案,專案名稱為KAndroid,包名為com.keegan.kandroid。預設已建立了app模組,檢視下app模組下的build.gradle,會看到第一行為:
1apply plugin: 'com.android.application'
這行表明了app模組是application型別的。 - 分別新建模組model、api、core,Module Type都選為Android Library,在Add an activity to module頁面選擇Add No Activity,這三個模組做為庫使用,並不需要介面。建立完之後,檢視相應模組的build.gradle,會看到第一行為:
1apply plugin: 'com.android.library' - 建立模組之間的依賴關係。有兩種方法可以設定:
第一種:通過右鍵模組,然後Open Module Settings,選擇模組的Dependencies,點選左下方的加號,選擇Module dependency,最後選擇要依賴的模組,下圖為api模組新增了model依賴;第二種:直接在模組的build.gradle設定。開啟build.gradle,在最後的dependencies一項裡面新增新的一行:compile project(‘:ModuleName’),比如app模組新增對model模組和core模組依賴之後的dependencies如下:
123456dependencies {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:1234dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.android.support:appcompat-v7:22.0.0'}api:
12345dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.android.support:appcompat-v7:22.0.0'compile project(':model')}core:
123456dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.android.support:appcompat-v7:22.0.0'compile project(':model')compile project(':api')}app:
123456dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.android.support:appcompat-v7:22.0.0'compile project(':model')compile project(':core')}
建立業務物件模型
業務物件模型統一存放於model模組,是對業務資料的封裝,大部分都是從介面傳過來的物件,因此,其屬性也與介面傳回的物件屬性相一致。在這個Demo裡,只有一個業務物件模型,封裝了券的基本資訊,以下是該實體類的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * 券的業務模型類,封裝了券的基本資訊。 * 券分為了三種型別:現金券、抵扣券、折扣券。 * 現金券是擁有固定面值的券,有固定的售價; * 抵扣券是滿足一定金額後可以抵扣的券,比如滿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 輸出樣例:
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 輸出樣例:
12345{ "event": "0", "msg": "success", "maxCount": 125, "maxPage": 7, "currentPage": 1, "pageSize": 20, "objList":[{"id": 1, "name": "測試現金券", "modelType": 1, ...},{...},...]}
在架構篇已經講過,介面返回的json資料有三種固定結構:
1 2 3 |
{"event": "0", "msg": "success"} {"event": "0", "msg": "success", "obj":{...}} {"event": "0", "msg": "success", "objList":[{...}, {...}], "currentPage": 1, "pageSize": 20, "maxCount": 2, "maxPage": 1} |
因此可以封裝成實體類,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class ApiResponseT> { 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個介面的定義如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
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 ApiResponseVoid> sendSmsCode4Register(String phoneNum); /** * 註冊 * * @param phoneNum 手機號碼 * @param code 驗證碼 * @param password MD5加密的密碼 * @return 成功時返回:{ "event": "0", "msg":"success" } */ public ApiResponseVoid> 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 ApiResponseVoid> loginByApp(String loginName, String password, String imei, int loginOS); /** * 券列表 * * @param currentPage 當前頁數 * @param pageSize 每頁顯示數量 * @return 成功時返回:{ "event": "0", "msg":"success", "objList":[...] } */ public ApiResponseListCouponBO>> listNewCoupon(int currentPage, int pageSize); } |
Api的實現類則是ApiImpl了,實現類需要封裝好請求資料並向伺服器發起請求,並將響應結果的資料轉為ApiResonse返回。而向伺服器傳送請求並將響應結果返回的處理則封裝到http引擎類去處理。另外,這裡引用了gson將json轉為物件。ApiImpl的實現程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
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 ApiResponseVoid> sendSmsCode4Register(String phoneNum) { MapString, String> paramMap = new HashMapString, String>(); paramMap.put("appKey", APP_KEY); paramMap.put("method", SEND_SMS_CODE); paramMap.put("phoneNum", phoneNum); Type type = new TypeTokenApiResponseVoid>>(){}.getType(); try { return httpEngine.postHandle(paramMap, type); } catch (IOException e) { return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG); } } @Override public ApiResponseVoid> registerByPhone(String phoneNum, String code, String password) { MapString, String> paramMap = new HashMapString, 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 TypeTokenApiResponseListCouponBO>>>(){}.getType(); try { return httpEngine.postHandle(paramMap, type); } catch (IOException e) { return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG); } } @Override public ApiResponseVoid> loginByApp(String loginName, String password, String imei, int loginOS) { MapString, String> paramMap = new HashMapString, 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 TypeTokenApiResponseListCouponBO>>>(){}.getType(); try { return httpEngine.postHandle(paramMap, type); } catch (IOException e) { return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG); } } @Override public ApiResponseListCouponBO>> listNewCoupon(int currentPage, int pageSize) { MapString, String> paramMap = new HashMapString, 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 TypeTokenApiResponseListCouponBO>>>(){}.getType(); try { return httpEngine.postHandle(paramMap, type); } catch (IOException e) { return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG); } } } |
而http引擎類的實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
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(MapString, 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(MapString, 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的定義:
1 2 3 4 5 6 7 8 9 10 |
public interface AppAction { // 傳送手機驗證碼 public void sendSmsCode(String phoneNum, ActionCallbackListenerVoid> listener); // 註冊 public void register(String phoneNum, String code, String password, ActionCallbackListenerVoid> listener); // 登入 public void login(String loginName, String password, ActionCallbackListenerVoid> listener); // 按分頁獲取券列表 public void listCoupon(int currentPage, ActionCallbackListenerListCouponBO>> listener); } |
首先,和Api介面對比就會發現,引數並不一致。登入並沒有iemi和loginOS的引數,獲取券列表的引數裡也少了pageSize。這是因為,這幾個引數,跟介面其實並沒有直接關係。Action只要定義好跟介面相關的就可以了,其他需要的引數,在具體實現時再去獲取。
另外,大部分action的處理都是非同步的,因此,新增了回撥監聽器ActionCallbackListener,回撥監聽器的泛型則是返回的物件資料型別,例如獲取券列表,返回的資料型別就是List,沒有物件資料時則為Void。回撥監聽器只定義了成功和失敗的方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public interface ActionCallbackListenerT> { /** * 成功時呼叫 * * @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。具體實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
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 ActionCallbackListenerVoid> 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 AsyncTaskVoid, Void, ApiResponseVoid>>() { @Override protected ApiResponseVoid> doInBackground(Void... voids) { return api.sendSmsCode4Register(phoneNum); } @Override protected void onPostExecute(ApiResponseVoid> 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 ActionCallbackListenerVoid> 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 AsyncTaskVoid, Void, ApiResponseVoid>>() { @Override protected ApiResponseVoid> doInBackground(Void... voids) { return api.registerByPhone(phoneNum, code, password); } @Override protected void onPostExecute(ApiResponseVoid> 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 ActionCallbackListenerVoid> 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 AsyncTaskVoid, Void, ApiResponseVoid>>() { @Override protected ApiResponseVoid> 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(ApiResponseVoid> 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 ActionCallbackListenerListCouponBO>> listener) { // 引數檢查 if (currentPage 0) { if (listener != null) { listener.onFailure(ErrorEvent.PARAM_ILLEGAL, "當前頁數小於零"); } } // TODO 新增快取 // 請求Api new AsyncTaskVoid, Void, ApiResponseListCouponBO>>>() { @Override protected ApiResponseListCouponBO>> doInBackground(Void... voids) { return api.listNewCoupon(currentPage, PAGE_SIZE); } @Override protected void onPostExecute(ApiResponseListCouponBO>> 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裡。程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class KApplication extends Application { private AppAction appAction; @Override public void onCreate() { super.onCreate(); appAction = new AppActionImpl(this); } public AppAction getAppAction() { return appAction; } } |
另外,一個Activity的基類也是很有必要的,可以減少很多重複的工作。基類的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
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 ActionCallbackListenerVoid>() { @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); } }); } } |
登入頁的佈局檔案則如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.keegan.kandroid.activity.LoginActivity"> 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" /> LinearLayout> |
可以看到,EditText的id命名統一以edit開頭,而在Activity裡的控制元件變數名則以Edit結尾。按鈕的onClick也統一用toXXX的方式命名,明確表明這是一個將要做的動作。還有,string,dimen也都統一在相應的資原始檔裡按照相應的規範去定義。
註冊頁和登陸頁差不多,這裡就不展示程式碼了。主要再看看券列表頁,因為用到了ListView,ListView需要新增介面卡。實際上,介面卡很多程式碼都是可以複用的,因此,我抽象了一個介面卡的基類,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
public abstract class KBaseAdapterT> extends BaseAdapter { protected Context context; protected LayoutInflater inflater; protected ListT> itemList = new ArrayListT>(); 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(ListT> itemList) { this.itemList.addAll(itemList); notifyDataSetChanged(); } /** * 設定為新的資料,舊資料會被清空 * * @param itemList */ public void setItems(ListT> 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的券列表的介面卡如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public class CouponListAdapter extends KBaseAdapterCouponBO> { 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簡單實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
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 ActionCallbackListenerListCouponBO>>() { @Override public void onSuccess(ListCouponBO> 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