淺談Android MVP設計模式(簡單結合RxJava+Retrofit)
轉載請指明出處:http://blog.csdn.net/lupengfei1009/article/details/50989066
這段時間看了不少基於MVP設計模式,然後結合RxJava+Retrofit寫的開源專案,深受感觸,為了能讓更多像我這種基層碼畜也能夠體驗一把大神們的世界,下面分享一點學習經驗。
什麼是MVP
model
處理業務邏輯,主要是資料讀寫,或者與後臺通訊,說通俗點就是取資料的地方。view
用於更新UI,由於Android中與使用者互動的只要是activity或fragment,所以,view一般就是值activity或fragmentpresenter
代理,用於協調管理model和view,通知model獲取資料,model獲取資料完之後,通知view更新介面
為什麼要用MVP
使用有一個最大的好處就是解耦,view就只負責更新UI,顯示控制元件,完成與使用者的互動;model的職責呢就是去載入資料;具體的model什麼時候去獲取資料,獲取完了之後ui什麼時候去更新,這一切都是由presenter去完成。這樣做,一方面適合團隊協作去開發,另一方面也方便測試,各個模組之間互不干擾。還有更多的好處和優點請自行百度。
怎麼去完成一個MVP的設計呢
所謂的MVP,其實說通俗一點就是將功能拆分成各個模組,各自完成之後通過回撥去做資料互動。前端(activity且已經實現了view介面)在例項化presenter的時候會將自己的view介面回撥告訴它(presenter),此時UI的管理權就交給了presenter;presenter在例項化model物件的例項的時候,會將自己的介面回撥傳遞給model物件(有人會說,這樣就其實是把管理presenter的管理權交給了model,為什麼這麼寫呢,因為網路請求都是需要耗時的,如果在presenter裡面去呼叫model方法,並等待返回的話,會造成執行緒阻塞;如果不是耗時的操作,那麼就可以在model中暴露方法讓presenter去獲取資料),model在獲取完資料之後,通過presenter的回撥告訴他,presenter在接受到model的資料之後又通過先前持有的view的回撥將資料或者提示等資訊告知UI並更新。如下圖:
接下來,我以一個通過介面獲取號碼歸屬地的例子去剖析一下MVP,同時簡單結合RxJava+Retrofit,最終效果如下圖。
demo例項,功能雖小,五臟俱全
準備工作
專案目錄結構,如下圖
獲取號碼歸屬地的介面及回覆的說明
介面地址:http://api.k780.com:88/?app=phone.get&phone=13888888888&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json
成功返回資料如下:
{
“success”: “1”,
“result”: {
“status”: “ALREADY_ATT”,
“phone”: “13888888888”,
“area”: “0871”,
“postno”: “650000”,
“att”: “中國,雲南,昆明”,
“ctype”: “中國移動138卡”,
“par”: “1388888”,
“prefix”: “138”,
“operators”: “中國移動”,
“style_simcall”: “中國,雲南,昆明”,
“style_citynm”: “中華人民共和國,雲南省,昆明市”
}
}失敗返回:
{
“success”: “0”,
“msgid”: “1000801”,
“msg”: “手機號碼不正確”
}
注:以上測試電話號碼為任意輸入的測試號碼,無任何其他用意,還請機主能諒解。定義收據歸屬地的實體類
以下的Bean是GsonFormat外掛自動生成,還沒有使用的朋友可以嘗試一下,很不錯的一款android studio外掛
import com.lpf.mvptest.base.BaseBean; /** * 電話號碼的歸屬地及其他資訊的物件 * Created by Administrator on 2016/3/23. */ public class PhoneNumInfo extends BaseBean { private ResultEntity result; /** * msg : 手機號碼不正確 * msgid : 1000801 */ private String msg; private String msgid; public ResultEntity getResult() { return result; } public void setResult(ResultEntity result) { this.result = result; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getMsgid() { return msgid; } public void setMsgid(String msgid) { this.msgid = msgid; } public static class ResultEntity { private String status; private String phone; private String area; private String postno; private String att; private String ctype; private String par; private String prefix; private String operators; private String style_simcall; private String style_citynm; public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getArea() { return area; } public void setArea(String area) { this.area = area; } public String getPostno() { return postno; } public void setPostno(String postno) { this.postno = postno; } public String getAtt() { return att; } public void setAtt(String att) { this.att = att; } public String getCtype() { return ctype; } public void setCtype(String ctype) { this.ctype = ctype; } public String getPar() { return par; } public void setPar(String par) { this.par = par; } public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getOperators() { return operators; } public void setOperators(String operators) { this.operators = operators; } public String getStyle_simcall() { return style_simcall; } public void setStyle_simcall(String style_simcall) { this.style_simcall = style_simcall; } public String getStyle_citynm() { return style_citynm; } public void setStyle_citynm(String style_citynm) { this.style_citynm = style_citynm; } @Override public String toString() { return "ResultEntity{" + "status='" + status + '\'' + ", phone='" + phone + '\'' + ", area='" + area + '\'' + ", postno='" + postno + '\'' + ", att='" + att + '\'' + ", ctype='" + ctype + '\'' + ", par='" + par + '\'' + ", prefix='" + prefix + '\'' + ", operators='" + operators + '\'' + ", style_simcall='" + style_simcall + '\'' + ", style_citynm='" + style_citynm + '\'' + '}'; } } @Override public String toString() { return "PhoneNumInfo{" + "success='" + success + '\'' + ", result=" + result + '}'; } }
View的基類
構建view的基礎介面,一個介面,請求一次資料基本都分為下面幾個步驟:顯示載入框–>載入資料成功(載入失敗)–>更新UI(提示使用者)–>關閉正在載入的框這麼5個事情,那麼我們就定義一個需要做這5件事兒的介面,由於是基類,所以返回的物件由具體的業務子類去定義就好。最終這個類的實現我們在activity或者fragment中去完成,以下為view介面的基類
/** * 檢視(View層)基礎回撥介面 */ public interface IBaseView<T> { /** * 通過toast提示使用者 * * @param msg 提示的資訊 * @param requestTag 請求標識 */ void toast(String msg, int requestTag); /** * 顯示進度 * * @param requestTag 請求標識 */ void showProgress(int requestTag); /** * 隱藏進度 * * @param requestTag 請求標識 */ void hideProgress(int requestTag); /** * 基礎的請求的返回 * * @param data * @param requestTag 請求標識 */ void loadDataSuccess(T data, int requestTag); /** * 基礎請求的錯誤 * * @param e * @param requestTag 請求標識 */ void loadDataError(Throwable e, int requestTag); }
presenter的基類
定義代理(presenter)回撥介面
/** * 請求資料的回撥<br> * Presenter用於接受model獲取(載入)資料後的回撥 * Created by Administrator on 2016/3/23. */ public interface IBasePresenter<T> { /** * 開始請求之前 */ void beforeRequest(int requestTag); /** * 請求失敗 * * @param e 失敗的原因 */ void requestError(Throwable e, int requestTag); /** * 請求結束 */ void requestComplete(int requestTag); /** * 請求成功 * * @param callBack 根據業務返回相應的資料 */ void retuestSuccess(T callBack, int requestTag); }
寫一個presenter的具體實現的基礎類BasePresenterImpl
或許會問,具體的實現放到具體的presenter的業務中去寫不就好了嘛,何必要在這裡寫一遍呢,又做不了什麼事情。NO!NO!NO!你想錯了,是否還記得前面定義的IBaseView,裡面定義了一些基本的UI操作;在這個BasePresenterImpl中,我們可以做一些基礎的事情(所有請求都會有的,比如:開啟Loading彈框、載入失敗後的錯誤提示),那麼就不用在每個子類裡面都要去寫一次。同時這個類接受2個泛型T,用於分別指定View檢視(T)及請求返回的結果(V)
/** * 代理物件的基礎實現 * * @param <T> 檢視介面物件(view) 具體業務各自繼承自IBaseView * @param <V> 業務請求返回的具體物件 */ public class BasePresenterImpl<T extends IBaseView, V> implements IBasePresenter<V> { public IBaseView iView; /** * 構造方法 * * @param view 具體業務的介面物件 */ public BasePresenterImpl(T view) { this.iView = view; } @Override public void beforeRequest(int requestTag) { //顯示LOading iView.showProgress(requestTag); } @Override public void requestError(Throwable e, int requestTag) { //通知UI具體的錯誤資訊 iView.loadDataError(e,requestTag); } @Override public void requestComplete(int requestTag) { //隱藏Loading iView.hideProgress(requestTag); } @Override public void retuestSuccess(V callBack, int requestTag) { //將獲取的資料回撥給UI(activity或者fragment) iView.loadDataSuccess(callBack, requestTag); } }
>程式碼中可以看到,載入前的彈框,載入成功回撥給UI,載入失敗通知UI錯誤資訊,載入完成關閉彈框等都已經在這裡做了一個基礎的實現。如果其中的方法不能滿足你的業務需求,你可以在具體業務的presenter實現中去重寫相應方法新增具體缺失的實現。
Model的基類
業務(model)類的基類
其中有寫到RetrofitManager這個類,在下面將會介紹,這是一個用於初始化retrofit和service的類,也就是model的輔助物件。
/** * 業務物件的基類 */ public class BaseModel { //retrofit請求資料的管理類 public RetrofitManager retrofitManager; public BaseModel() { //初始化retrofit retrofitManager = RetrofitManager.builder(); } }
定義一個請求歸屬地的服務
/** * 歸屬地請求的服務 */ public interface PhoneNunInfoService { //http://api.k780.com:88/?app=phone.get&phone={phoneNum}&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json @Headers(RetrofitManager.CACHE_CONTROL_AGE + RetrofitManager.CACHE_STALE_LONG) @GET("/") Observable<PhoneNumInfo> getBeforeNews(@Query("app") String app , @Query("phone") String phone , @Query("appkey") String appkey , @Query("sign") String sign , @Query("format") String format); }
建立RetrofitManager物件,他的作用就是初始化retrofit、service以及新增快取機制(如果不理解可以不關注他,將那一塊的程式碼註釋掉依然是可以執行的)
/** * Retrofit管理類 */ public class RetrofitManager { //地址 public static final String BASE_PHONENUMINFO_URL = "http://api.k780.com:88"; //短快取有效期為1分鐘 public static final int CACHE_STALE_SHORT = 60; //長快取有效期為7天 public static final int CACHE_STALE_LONG = 60 * 60 * 24 * 7; public static final String CACHE_CONTROL_AGE = "Cache-Control: public, max-age="; //查詢快取的Cache-Control設定,為if-only-cache時只查詢快取而不會請求伺服器,max-stale可以配合設定快取失效時間 public static final String CACHE_CONTROL_CACHE = "only-if-cached, max-stale=" + CACHE_STALE_LONG; //查詢網路的Cache-Control設定,頭部Cache-Control設為max-age=0時則不會使用快取而請求伺服器 public static final String CACHE_CONTROL_NETWORK = "max-age=0"; private static OkHttpClient mOkHttpClient; private final PhoneNunInfoService phoneNunInfoService; public static RetrofitManager builder() { return new RetrofitManager(); } public PhoneNunInfoService getService() { return phoneNunInfoService; } private RetrofitManager() { initOkHttpClient(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_PHONENUMINFO_URL) .client(mOkHttpClient) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); phoneNunInfoService = retrofit.create(PhoneNunInfoService.class); } private void initOkHttpClient() { HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); if (mOkHttpClient == null) { synchronized (RetrofitManager.class) { if (mOkHttpClient == null) { // 指定快取路徑,快取大小100Mb Cache cache = new Cache(new File(MyApplication.getContext().getCacheDir(), "HttpCache"), 1024 * 1024 * 100); mOkHttpClient = new OkHttpClient.Builder() .cache(cache) .addInterceptor(mRewriteCacheControlInterceptor) .addNetworkInterceptor(mRewriteCacheControlInterceptor) .addInterceptor(interceptor) .addNetworkInterceptor(new StethoInterceptor()) .retryOnConnectionFailure(true) .connectTimeout(15, TimeUnit.SECONDS) .build(); } } } } // 雲端響應頭攔截器,用來配置快取策略 private Interceptor mRewriteCacheControlInterceptor = new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); if (!NetUtil.isNetworkConnected()) { request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build(); } Response originalResponse = chain.proceed(request); if (NetUtil.isNetworkConnected()) { //有網的時候讀介面上的@Headers裡的配置,你可以在這裡進行統一的設定 String cacheControl = request.cacheControl().toString(); return originalResponse.newBuilder().header("Cache-Control", cacheControl) .removeHeader("Pragma").build(); } else { return originalResponse.newBuilder() .header("Cache-Control", "public, only-if-cached, max-stale=" + CACHE_STALE_LONG) .removeHeader("Pragma").build(); } } }; }
到此,基礎的東西已經完成了,這樣的事情,雖然說在實現這樣的小功能中確實是顯的有些多餘了,但是在實際的開發過程中,我們並不是只做一件事,只做一個請求,只顯示一個介面,因此,這樣往上抽一層還是很有必要的。下面將回歸到真正寫號碼歸屬地的實際業務中來
具體的業務實現
查詢號碼歸屬地的view
由於基礎的view介面已經可以滿足號碼歸屬地查詢的ui更新使用了,同時也為了專案方便管理,便於理解,我們新建一個PhoneNumInfoView,繼承IBaseView,並告訴baseView請求成功後需要返回一個PhoneNumInfo的物件。
/** * 號碼歸屬地查詢的View */ public interface PhoneNumInfoView extends IBaseView<PhoneNumInfo>{ }
定義獲取歸屬地的Model
/** * 獲取號碼歸屬地的具體Model實現 */ public class PhoneModelImpl extends BaseModel { private Context mContext; private PhoneNunInfoService phoneNunInfoService; public PhoneModelImpl(Context context) { super(); this.mContext = context; phoneNunInfoService = retrofitManager.getService(); } public void loadPhoneNumInfo(String phoneNum, final IBasePresenter<PhoneNumInfo> callBack, final int requestTag) { phoneNunInfoService.getBeforeNews("phone.get", phoneNum, "10003", "b59bc3ef6191eb9f747dd4e83c99f2a4", "json") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<PhoneNumInfo>() { @Override public void onStart() { super.onStart(); callBack.beforeRequest(requestTag); } @Override public void onCompleted() { callBack.requestComplete(requestTag); } @Override public void onError(Throwable e) { callBack.requestError(e, requestTag); } @Override public void onNext(PhoneNumInfo phoneNumInfo) { if (null != phoneNumInfo && phoneNumInfo.getSuccess().equals("1")) callBack.retuestSuccess(phoneNumInfo, requestTag); else if (null != phoneNumInfo && phoneNumInfo.getSuccess().equals("0")) callBack.requestError(new Exception(phoneNumInfo.getMsg()), requestTag); else callBack.requestError(new Exception("獲取資料錯誤,請重試!"), requestTag); } }); } }
定義一個用於獲取號碼歸屬地的代理物件(presenter)
在例項化代理物件的時候拿到前端(activity或者fragment)的view例項,並初始化Model物件,同時對外提供一個獲取資料的方法,方便activity去獲取資料。
/** * Presenter的實現,協調model去載入資料,獲取model載入完成時候的回撥,控制介面載入框的顯示與隱藏 */ public class PhonePresenterImpl extends BasePresenterImpl<PhoneNumInfoView, PhoneNumInfo> { private PhoneModelImpl phoneModel; private Context mContext; public PhonePresenterImpl(PhoneNumInfoView phoneNumInfoView, Context context) { super(phoneNumInfoView); this.mContext = context; phoneModel = new PhoneModelImpl(mContext); } /** * 獲取歸屬地資訊 * * @param phoneNum 電話號碼 * @param requestTag 請求標識 */ public void getPhoneNumInfo(String phoneNum, int requestTag) { phoneModel.loadPhoneNumInfo(phoneNum, this, requestTag); } }
實現activity中相關的程式碼
前端activity的程式碼並不多,就是實現PhoneNumInfoView的相應介面,然後最基礎的獲取控制元件,例項化presenter,把當前的view例項以引數的形式傳遞給presenter;介面的實現也就是要顯示Loading的地方把框show出來,在獲取資料成功的地方顯示資料等等,各負其職。然後在按鈕的點選事件的地方呼叫presenter中相應的獲取資料的方法。具體的什麼時候顯示Loading,什麼時候執行載入完成的操作等,就不需要activity去管了,安安心心的全權交個presenter去做就好了。程式碼如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener, PhoneNumInfoView { //獲取歸屬地的請求標識,用回頁面多個地方請求的時候,標識是那一個完成並回調了 private static final int REQUESTMSG = 0; //電話號碼的EditText private EditText phoneNum; //獲取歸屬地的按鈕 private Button getPhoneInfo; //用於顯示最後獲取的結果 private TextView msg; //Loading彈框 private ProgressDialog progressDialog; //獲取歸屬地的代理物件 private PhonePresenterImpl phonePresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { //初始化控制元件 phoneNum = (EditText) findViewById(R.id.phonenum); getPhoneInfo = (Button) findViewById(R.id.getphoneinfo); msg = (TextView) findViewById(R.id.msg); getPhoneInfo.setOnClickListener(this); //初始化Loading progressDialog = new ProgressDialog(this); progressDialog.setMessage("Loading"); //初始化代理物件 phonePresenter = new PhonePresenterImpl(this, this); } @Override public void toast(String msg, int requestTag) { } @Override public void showProgress(int requestTag) { if (null != progressDialog && !progressDialog.isShowing()) { progressDialog.show(); } } @Override public void hideProgress(int requestTag) { if (null != progressDialog && progressDialog.isShowing()) { progressDialog.dismiss(); } } @Override public void loadDataSuccess(PhoneNumInfo phoneNumInfo, int requestTag) { msg.setText(phoneNumInfo.toString()); } @Override public void loadDataError(Throwable e, int requestTag) { msg.setText(e.getMessage()); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.getphoneinfo: phonePresenter.getPhoneNumInfo(phoneNum.getText().toString(), REQUESTMSG); break; } } }
效果圖
成功
失敗
到這裡,一個基於MVP的DEMO就寫完了,其中有用到RxJava和Retrofit,但是沒有做明確的說明,如果想了解可以閱讀:RxJava 與 Retrofit 結合的最佳實踐
原始碼下載地址戳這裡
2017年2月19日,去掉冗餘程式碼,簡化了流程。