日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

Android項(xiàng)目重構(gòu)之路:實(shí)現(xiàn)篇

 昵稱30481029 2016-02-14

原創(chuàng)文章,轉(zhuǎn)載請注明:轉(zhuǎn)載自Keegan小鋼
微信訂閱號:keeganlee_me
寫于2015-06-28


Android項(xiàng)目重構(gòu)之路:架構(gòu)篇
Android項(xiàng)目重構(gòu)之路:界面篇
Android項(xiàng)目重構(gòu)之路:實(shí)現(xiàn)篇


前兩篇文章Android項(xiàng)目重構(gòu)之路:架構(gòu)篇Android項(xiàng)目重構(gòu)之路:界面篇已經(jīng)講了我的項(xiàng)目開始搭建時(shí)的架構(gòu)設(shè)計(jì)和界面設(shè)計(jì),這篇就講講具體怎么實(shí)現(xiàn)的,以實(shí)現(xiàn)最小化可用產(chǎn)品(MVP)的目標(biāo),用最簡單的方式來搭建架構(gòu)和實(shí)現(xiàn)代碼。
IDE采用Android Studio,Demo實(shí)現(xiàn)的功能為用戶注冊、登錄和展示一個(gè)券列表,數(shù)據(jù)采用我們現(xiàn)有項(xiàng)目的測試數(shù)據(jù),接口也是我們項(xiàng)目中的測試接口。

項(xiàng)目搭建

根據(jù)架構(gòu)篇所講的,將項(xiàng)目分為了四個(gè)層級:模型層、接口層、核心層、界面層。四個(gè)層級之間的關(guān)系如下圖所示:

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

  1. 創(chuàng)建新項(xiàng)目,項(xiàng)目名稱為KAndroid,包名為com.keegan.kandroid。默認(rèn)已創(chuàng)建了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,這三個(gè)模塊做為庫使用,并不需要界面。創(chuàng)建完之后,查看相應(yīng)模塊的build.gradle,會看到第一行為:

    apply plugin: 'com.android.library'
        
  3. 建立模塊之間的依賴關(guān)系。有兩種方法可以設(shè)置:
    第一種:通過右鍵模塊,然后Open Module Settings,選擇模塊的Dependencies,點(diǎn)擊左下方的加號,選擇Module dependency,最后選擇要依賴的模塊,下圖為api模塊添加了model依賴;

    第二種:直接在模塊的build.gradle設(shè)置。打開build.gradle,在最后的dependencies一項(xiàng)里面添加新的一行: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')
        }
        

    通過上面兩種方式的任意一種,創(chuàng)建了模塊之間的依賴關(guān)系之后,每個(gè)模塊的build.gradle的dependencies項(xiàng)的結(jié)果將會如下:
    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')
        }
        

創(chuàng)建業(yè)務(wù)對象模型

業(yè)務(wù)對象模型統(tǒng)一存放于model模塊,是對業(yè)務(wù)數(shù)據(jù)的封裝,大部分都是從接口傳過來的對象,因此,其屬性也與接口傳回的對象屬性相一致。在這個(gè)Demo里,只有一個(gè)業(yè)務(wù)對象模型,封裝了券的基本信息,以下是該實(shí)體類的代碼:

/**
 * 券的業(yè)務(wù)模型類,封裝了券的基本信息。
 * 券分為了三種類型:現(xiàn)金券、抵扣券、折扣券。
 * 現(xiàn)金券是擁有固定面值的券,有固定的售價(jià);
 * 抵扣券是滿足一定金額后可以抵扣的券,比如滿100減10元;
 * 折扣券是可以打折的券。
 *
 * @version 1.0 創(chuàng)建時(shí)間: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為現(xiàn)金券,2為抵扣券,3為折扣券
private double faceValue;      // 現(xiàn)金券的面值
private double estimateAmount; // 現(xiàn)金券的售價(jià)
private double debitAmount;    // 抵扣券的抵扣金額
private double discount;       // 折扣券的折扣率(0-100)
private double miniAmount;     // 抵扣券和折扣券的最小使用金額
// TODO 所有屬性的getter和setter
}

接口層的封裝

在這個(gè)Demo里,提供了4個(gè)接口:一個(gè)發(fā)送驗(yàn)證碼的接口、一個(gè)注冊接口、一個(gè)登錄接口、一個(gè)獲取券列表的接口。這4個(gè)接口具體如下:

  • 發(fā)送驗(yàn)證碼接口
    URL:http://uat.b./platform/api
    參數(shù):

    參數(shù)名 描述 類型
    appKey ANDROID_KCOUPON String
    method service.sendSmsCode4Register String
    phoneNum 手機(jī)號碼 String

    輸出樣例:

    { 'event': '0', 'msg': 'success' }
        
  • 注冊接口
    URL:http://uat.b./platform/api
    參數(shù):

    參數(shù)名 描述 類型
    appKey ANDROID_KCOUPON String
    method customer.registerByPhone String
    phoneNum 手機(jī)號碼 String
    code 驗(yàn)證碼 String
    password MD5加密密碼 String

    輸出樣例:

    { 'event': '0', 'msg': 'success' }
        
  • 登錄接口
    URL:http://uat.b./platform/api
    其他參數(shù):

    參數(shù)名 描述 類型
    appKey ANDROID_KCOUPON String
    method customer.loginByApp String
    loginName 登錄名(手機(jī)號) String
    password MD5加密密碼 String
    imei 手機(jī)imei串號 String
    loginOS 系統(tǒng),android為1 int

    輸出樣例:

    { 'event': '0', 'msg': 'success' }
        
  • 券列表
    URL:http://uat.b./platform/api
    其他參數(shù):

    參數(shù)名 描述 類型
    appKey ANDROID_KCOUPON String
    method issue.listNewCoupon String
    currentPage 當(dāng)前頁數(shù) int
    pageSize 每頁顯示數(shù)量 int

    輸出樣例:

    { 'event': '0', 'msg': 'success', 'maxCount': 125, 'maxPage': 7, 'currentPage': 1, 'pageSize': 20, 'objList':[
        {'id': 1, 'name': '測試現(xiàn)金券', 'modelType': 1, ...},
        {...},
        ...
        ]}
        

架構(gòu)篇已經(jīng)講過,接口返回的json數(shù)據(jù)有三種固定結(jié)構(gòu):

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

因此可以封裝成實(shí)體類,代碼如下:

public class ApiResponse<T> {
private String event;    // 返回碼,0為成功
private String msg;      // 返回信息
private T obj;           // 單個(gè)對象
private T objList;       // 數(shù)組對象
private int currentPage; // 當(dāng)前頁數(shù)
private int pageSize;    // 每頁顯示數(shù)量
private int maxCount;    // 總條數(shù)
private int maxPage;     // 總頁數(shù)
// 構(gòu)造函數(shù),初始化code和msg
public ApiResponse(String event, String msg) {
this.event = event;
this.msg = msg;
}
// 判斷結(jié)果是否成功
public boolean isSuccess() {
return event.equals('0');
}
// TODO 所有屬性的getter和setter
}

上面4個(gè)接口,URL和appKey都是一樣的,用來區(qū)別不同接口的則是method字段,因此,URL和appKey可以統(tǒng)一定義,method則根據(jù)不同接口定義不同常量。而除去appKey和method,剩下的參數(shù)才是每個(gè)接口需要定義的參數(shù)。因此,對上面4個(gè)接口的定義如下:

public interface Api {
// 發(fā)送驗(yàn)證碼
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';
/**
     * 發(fā)送驗(yàn)證碼
     *
     * @param phoneNum 手機(jī)號碼
     * @return 成功時(shí)返回:{ 'event': '0', 'msg':'success' }
     */
public ApiResponse<Void> sendSmsCode4Register(String phoneNum);
/**
     * 注冊
     *
     * @param phoneNum 手機(jī)號碼
     * @param code     驗(yàn)證碼
     * @param password MD5加密的密碼
     * @return 成功時(shí)返回:{ 'event': '0', 'msg':'success' }
     */
public ApiResponse<Void> registerByPhone(String phoneNum, String code, String password);
/**
     * 登錄
     *
     * @param loginName 登錄名(手機(jī)號)
     * @param password  MD5加密的密碼
     * @param imei      手機(jī)IMEI串號
     * @param loginOS   Android為1
     * @return 成功時(shí)返回:{ 'event': '0', 'msg':'success' }
     */
public ApiResponse<Void> loginByApp(String loginName, String password, String imei, int loginOS);
/**
     * 券列表
     *
     * @param currentPage 當(dāng)前頁數(shù)
     * @param pageSize    每頁顯示數(shù)量
     * @return 成功時(shí)返回:{ 'event': '0', 'msg':'success', 'objList':[...] }
     */
public ApiResponse<List<CouponBO>> listNewCoupon(int currentPage, int pageSize);
}

Api的實(shí)現(xiàn)類則是ApiImpl了,實(shí)現(xiàn)類需要封裝好請求數(shù)據(jù)并向服務(wù)器發(fā)起請求,并將響應(yīng)結(jié)果的數(shù)據(jù)轉(zhuǎn)為ApiResonse返回。而向服務(wù)器發(fā)送請求并將響應(yīng)結(jié)果返回的處理則封裝到http引擎類去處理。另外,這里引用了gson將json轉(zhuǎn)為對象。ApiImpl的實(shí)現(xiàn)代碼如下:

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 = '連接服務(wù)器失敗';
// 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引擎類的實(shí)現(xiàn)如下:

public class HttpEngine {
private final static String SERVER_URL = 'http://uat.b./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) {
// 獲取響應(yīng)的輸入流對象
InputStream is = connection.getInputStream();
// 創(chuàng)建字節(jié)輸出流對象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 定義讀取的長度
int len = 0;
// 定義緩沖區(qū)
byte buffer[] = new byte[1024];
// 按照緩沖區(qū)的大小,循環(huán)讀取
while ((len = is.read(buffer)) != -1) {
// 根據(jù)讀取的長度寫入到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 {
// 根據(jù)地址創(chuàng)建URL對象
URL url = new URL(SERVER_URL);
// 根據(jù)URL對象打開鏈接
connection = (HttpURLConnection) url.openConnection();
// 設(shè)置請求的方式
connection.setRequestMethod(REQUEST_MOTHOD);
// 發(fā)送POST請求必須設(shè)置允許輸入,默認(rèn)為true
connection.setDoInput(true);
// 發(fā)送POST請求必須設(shè)置允許輸出
connection.setDoOutput(true);
// 設(shè)置不使用緩存
connection.setUseCaches(false);
// 設(shè)置請求的超時(shí)時(shí)間
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);
}
}

至此,接口層的封裝就完成了。接下來再往上看看核心層吧。

核心層的邏輯

核心層處于接口層和界面層之間,向下調(diào)用Api,向上提供Action,它的核心任務(wù)就是處理復(fù)雜的業(yè)務(wù)邏輯。先看看我對Action的定義:

public interface AppAction {
// 發(fā)送手機(jī)驗(yàn)證碼
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接口對比就會發(fā)現(xiàn),參數(shù)并不一致。登錄并沒有iemi和loginOS的參數(shù),獲取券列表的參數(shù)里也少了pageSize。這是因?yàn)椋@幾個(gè)參數(shù),跟界面其實(shí)并沒有直接關(guān)系。Action只要定義好跟界面相關(guān)的就可以了,其他需要的參數(shù),在具體實(shí)現(xiàn)時(shí)再去獲取。
另外,大部分action的處理都是異步的,因此,添加了回調(diào)監(jiān)聽器ActionCallbackListener,回調(diào)監(jiān)聽器的泛型則是返回的對象數(shù)據(jù)類型,例如獲取券列表,返回的數(shù)據(jù)類型就是List,沒有對象數(shù)據(jù)時(shí)則為Void?;卣{(diào)監(jiān)聽器只定義了成功和失敗的方法,如下:

public interface ActionCallbackListener<T> {
/**
     * 成功時(shí)調(diào)用
     *
     * @param data 返回的數(shù)據(jù)
     */
public void onSuccess(T data);
/**
     * 失敗時(shí)調(diào)用
     *
     * @param errorEvemt 錯(cuò)誤碼
     * @param message    錯(cuò)誤信息
     */
public void onFailure(String errorEvent, String message);
}

接下來再看看Action的實(shí)現(xiàn)。首先,要獲取imei,那就需要傳入一個(gè)Context;另外,還需要loginOS和pageSize,這定義為常量就可以了;還有,要調(diào)用接口層,所以還需要Api實(shí)例。而接口的實(shí)現(xiàn)分為兩步,第一步做參數(shù)檢查,第二步用異步任務(wù)調(diào)用Api。具體實(shí)現(xiàn)如下:

public class AppActionImpl implements AppAction {
private final static int LOGIN_OS = 1; // 表示Android
private final static int PAGE_SIZE = 20; // 默認(rèn)每頁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) {
// 參數(shù)為空檢查
if (TextUtils.isEmpty(phoneNum)) {
if (listener != null) {
listener.onFailure(ErrorEvent.PARAM_NULL, '手機(jī)號為空');
}
return;
}
// 參數(shù)合法性檢查
Pattern pattern = Pattern.compile('1\\d{10}');
Matcher matcher = pattern.matcher(phoneNum);
if (!matcher.matches()) {
if (listener != null) {
listener.onFailure(ErrorEvent.PARAM_ILLEGAL, '手機(jī)號不正確');
}
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) {
// 參數(shù)為空檢查
if (TextUtils.isEmpty(phoneNum)) {
if (listener != null) {
listener.onFailure(ErrorEvent.PARAM_NULL, '手機(jī)號為空');
}
return;
}
if (TextUtils.isEmpty(code)) {
if (listener != null) {
listener.onFailure(ErrorEvent.PARAM_NULL, '驗(yàn)證碼為空');
}
return;
}
if (TextUtils.isEmpty(password)) {
if (listener != null) {
listener.onFailure(ErrorEvent.PARAM_NULL, '密碼為空');
}
return;
}
// 參數(shù)合法性檢查
Pattern pattern = Pattern.compile('1\\d{10}');
Matcher matcher = pattern.matcher(phoneNum);
if (!matcher.matches()) {
if (listener != null) {
listener.onFailure(ErrorEvent.PARAM_ILLEGAL, '手機(jī)號不正確');
}
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) {
// 參數(shù)為空檢查
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) {
// 參數(shù)檢查
if (currentPage < 0) {
if (listener != null) {
listener.onFailure(ErrorEvent.PARAM_ILLEGAL, '當(dāng)前頁數(shù)小于零');
}
}
// 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();
}
}

簡單的實(shí)現(xiàn)代碼就是這樣,其實(shí),這還有很多地方可以優(yōu)化,比如,將參數(shù)為空的檢查、手機(jī)號有效性的檢查、數(shù)字型范圍的檢查等等,都可以抽成獨(dú)立的方法,從而減少重復(fù)代碼的編寫。異步任務(wù)里的代碼也一樣,都是可以通過重構(gòu)優(yōu)化的。另外,需要擴(kuò)展時(shí),比如添加緩存,那就在調(diào)用Api之前處理。
核心層的邏輯就是這樣了。最后就到界面層了。

界面層

在這個(gè)Demo里,只有三個(gè)頁面:登錄頁、注冊頁、券列表頁。在這里,也會遵循界面篇提到的三個(gè)基本原則:規(guī)范性、單一性、簡潔性。
首先,界面層需要調(diào)用核心層的Action,而這會在整個(gè)應(yīng)用級別都用到,因此,Action的實(shí)例最好放在Application里。代碼如下:

public class KApplication extends Application {
private AppAction appAction;
@Override
public void onCreate() {
super.onCreate();
appAction = new AppActionImpl(this);
}
public AppAction getAppAction() {
return appAction;
}
}

另外,一個(gè)Activity的基類也是很有必要的,可以減少很多重復(fù)的工作?;惖拇a如下:

public abstract class KBaseActivity extends FragmentActivity {
// 上下文實(shí)例
public Context context;
// 應(yīng)用全局的實(shí)例
public KApplication application;
// 核心層的Action實(shí)例
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);
}
// 準(zhǔn)備登錄
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./apk/res/android'
xmlns:tools='http://schemas./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命名統(tǒng)一以edit開頭,而在Activity里的控件變量名則以Edit結(jié)尾。按鈕的onClick也統(tǒng)一用toXXX的方式命名,明確表明這是一個(gè)將要做的動(dòng)作。還有,string,dimen也都統(tǒng)一在相應(yīng)的資源文件里按照相應(yīng)的規(guī)范去定義。
注冊頁和登陸頁差不多,這里就不展示代碼了。主要再看看券列表頁,因?yàn)橛玫搅薒istView,ListView需要添加適配器。實(shí)際上,適配器很多代碼都是可以復(fù)用的,因此,我抽象了一個(gè)適配器的基類,代碼如下:

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);
}
/**
     * 判斷數(shù)據(jù)是否為空
     *
     * @return 為空返回true,不為空返回false
     */
public boolean isEmpty() {
return itemList.isEmpty();
}
/**
     * 在原有的數(shù)據(jù)上添加新數(shù)據(jù)
     *
     * @param itemList
     */
public void addItems(List<T> itemList) {
this.itemList.addAll(itemList);
notifyDataSetChanged();
}
/**
     * 設(shè)置為新的數(shù)據(jù),舊數(shù)據(jù)會被清空
     *
     * @param itemList
     */
public void setItems(List<T> itemList) {
this.itemList.clear();
this.itemList = itemList;
notifyDataSetChanged();
}
/**
     * 清空數(shù)據(jù)
     */
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);
}

這個(gè)抽象基類集成了設(shè)置數(shù)據(jù)的方法,每個(gè)具體的適配器類只要再實(shí)現(xiàn)各自的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;
// 根據(jù)不同的券類型展示不同的價(jià)格顯示方式
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簡單實(shí)現(xiàn)如下:

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 { // 分頁數(shù)據(jù)
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() {
// 需要重置當(dāng)前頁為第一頁,并且清掉數(shù)據(jù)
currentPage = 1;
listAdapter.clearItems();
getData();
}
}

完結(jié)

終于寫完了,代碼也終于放上了github,為了讓人更容易理解,因此很多都比較簡單,沒有再進(jìn)行擴(kuò)展。
github地址:https://github.com/keeganlee/kandroid


掃描以下二維碼即可關(guān)注訂閱號。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多