Android項目重構之路:實現篇

Android項目重構之路:實現篇

原創文章,轉載請註明:轉載自Keegan小鋼
並標明原文連接:http://keeganlee.me/post/android/20150629
微信訂閱號:keeganlee_me
寫於2015-06-29android


Android項目重構之路:架構篇
Android項目重構之路:界面篇
Android項目重構之路:實現篇git


前兩篇文章Android項目重構之路:架構篇Android項目重構之路:界面篇已經講了個人項目開始搭建時的架構設計和界面設計,這篇就講講具體怎麼實現的,以實現最小化可用產品(MVP)的目標,用最簡單的方式來搭建架構和實現代碼。
IDE採用Android Studio,Demo實現的功能爲用戶註冊、登陸和展現一個券列表,數據採用咱們現有項目的測試數據,接口也是咱們項目中的測試接口。github

項目搭建

根據架構篇所講的,將項目分爲了四個層級:模型層、接口層、核心層、界面層。四個層級之間的關係以下圖所示:json

實現上,在Android Studio分爲了相應的四個模塊(Module):model、api、core、app
model爲模型層,api爲接口層,core爲核心層,app爲界面層。
model、api、core這三個模塊的類型爲library,app模塊的類型爲application。
四個模塊之間的依賴設置爲:model沒有任何依賴,接口層依賴了模型層,核心層依賴了模型層和接口層,界面層依賴了核心層和模型層。
項目搭建的步驟以下:api

  1. 建立新項目,項目名稱爲KAndroid,包名爲com.keegan.kandroid。默認已建立了app模塊,查看下app模塊下的build.gradle,會看到第一行爲:數組

    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,會看到第一行爲:服務器

    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以下:架構

    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<T> { 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<Void> listener); // 註冊 public void register(String phoneNum, String code, String password, ActionCallbackListener<Void> listener); // 登陸 public void login(String loginName, String password, ActionCallbackListener<Void> listener); // 按分頁獲取券列表 public void listCoupon(int currentPage, ActionCallbackListener<List<CouponBO>> listener); } 

首先,和Api接口對比就會發現,參數並不一致。登陸並無iemi和loginOS的參數,獲取券列表的參數裏也少了pageSize。這是由於,這幾個參數,跟界面其實並無直接關係。Action只要定義好跟界面相關的就能夠了,其餘須要的參數,在具體實現時再去獲取。
另外,大部分action的處理都是異步的,所以,添加了回調監聽器ActionCallbackListener,回調監聽器的泛型則是返回的對象數據類型,例如獲取券列表,返回的數據類型就是List,沒有對象數據時則爲Void。回調監聽器只定義了成功和失敗的方法,以下:

public interface ActionCallbackListener<T> { /**  * 成功時調用  *  * @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); } }); } } 

登陸頁的佈局文件則以下:

<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須要添加適配器。實際上,適配器不少代碼都是能夠複用的,所以,我抽象了一個適配器的基類,代碼以下:

public abstract class KBaseAdapter<T> 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<CouponBO> { 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


掃描如下二維碼便可關注訂閱號。

相關文章
相關標籤/搜索