1. 程式人生 > >淺談Android MVP設計模式(簡單結合RxJava+Retrofit)

淺談Android MVP設計模式(簡單結合RxJava+Retrofit)

轉載請指明出處:http://blog.csdn.net/lupengfei1009/article/details/50989066

這段時間看了不少基於MVP設計模式,然後結合RxJava+Retrofit寫的開源專案,深受感觸,為了能讓更多像我這種基層碼畜也能夠體驗一把大神們的世界,下面分享一點學習經驗。

什麼是MVP

  • model
    處理業務邏輯,主要是資料讀寫,或者與後臺通訊,說通俗點就是取資料的地方。

  • view
    用於更新UI,由於Android中與使用者互動的只要是activity或fragment,所以,view一般就是值activity或fragment

  • presenter
    代理,用於協調管理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日,去掉冗餘程式碼,簡化了流程。