Android 實現(xiàn)一個 MVP 基礎框架

Kotlin 版的可參考:https://github.com/SheHuan/WanAndroid

這幾年 MVP 在 Android 開發(fā)中已經開始被廣泛使用跋破,逐漸成為一種主流的設計思想瓶蝴。在 MVP 出現(xiàn)之前舷手,我們使用最多的可能就是 MVC 了男窟,那么我們?yōu)槭裁匆褂?MVP,它解決了 MVC 使用中的那些痛點呢弟劲,那我們先從 MVC 說起兔乞。

一凉唐、淺談 MVC

MVC 的全稱是 Model-View-Controller台囱,這三部分在 Android 中可以按照如下的層次劃分:

  • Model(數(shù)據模型層)
    主要負責網絡請求簿训、數(shù)據庫等其它 I/O 操作
  • View(視圖層)
    主要包括 XML 布局文件(但功能較弱)
  • Controller(控制器層)
    一般就是 Activity 或者 Fragment米间,負責 MVC 整個流程的調度屈糊、協(xié)作逻锐。但是由于 XML 的視圖處理能力較弱雕薪,需要 Activity 或者 Fragment 承擔部分視圖層工作所袁。

在 Android 中可以用網絡請求的例子來梳理一下 MVC 的流程纲熏,例如,在 Activity(Controller)中通過一個 Button 點擊事件調用 Model 層代碼發(fā)起網絡請求勺拣,當 Model 層網絡請求成功響應后药有,在對應的回調函數(shù)中去更新 View 層的一個 RecyclerView愤惰。按照事件的流向赘理,例子中 MVC 三部分的關系如下:

MVC

你可能還見過不同的 MVC 關系圖商模,其實都是對的施流,因為不同的 MVC 實現(xiàn)可能會有不同的事件流向瞪醋,選擇適合自己的就行。

上圖看起來層次結構很清晰践盼,但還是存在一些問題的咕幻,雖然我們可以把網絡請求的 Model 層解耦成一個單獨的模塊谅河,但從網絡請求的發(fā)起到響應結果的處理都集中在了 Activity 中确丢,View 層的功能基本被弱化的可以忽略了鲜侥,基本由 Activity 承擔了描函,所以 Activity 同時承擔了 Controller 和 View 層的職責,帶來了耦合的問題胆数,隨著業(yè)務量的增加必然會導致 Activity 變的臃腫起來必尼,增加維護成本判莉。

那么面對這些問題 MVP 是如何應對的呢券盅,它和 MVC 有何異同呢......

二膛檀、更好的 MVP

MVP 的全稱是 Model-View-Presenter宿刮,它保留了 MVC 中的 View 層和 Model 層僵缺,用 Presenter 層替代了 Controller 層磕潮,在 Android 開發(fā)中三個層次的職責劃分如下:

  • Model(數(shù)據模型層)
    主要負責網絡請求容贝、數(shù)據庫等其它 I/O 操作斤富,和 MVC 中的 Model 層類似
  • View(視圖層)
    主要是 Activity满力、Fragment轻纪、XML布局文件
  • Presenter(控制器層)
    自定義的 Presenter 類刻帚,類似于 MVC 中 Controller 的作用崇众,是 MVP 中的核心顷歌,主要負責 Model 層和 View 層的交互

在 MVP 中 Activity衙吩、Fragment、XML都歸屬到了 View 層冯勉,即不存在 MVC 中 V灼狰、C層之間耦合的問題交胚。在 MVC 中蝴簇,Model 層功能代碼的調用是在 Activity匆帚、Fragment 中進行的,但在 MVP 中這個職責將由 Presenter 去完成歪今,然后將結果通過接口傳遞給 View 層寄猩,可以將 Model 層的實現(xiàn)田篇、調用對 View 層隱藏起來斯辰,這樣可以有效的降低 Activity坡疼、Fragment 的代碼量柄瑰,避免代碼臃腫教沾。

所以相比 MVC授翻,MVP 的職責劃分更加清晰堪唐,能夠更好額解耦淮菠,但也會增加一些類文件荤堪。

下邊是一個 MVP 的事件流向圖澄阳,可以看到碎赢,在 MVP 中我們約定 Model 層和 View 層是不能直接通信的揩抡,而是通過 Presenter 層完成交互。

MVP

三屋摔、簡單上手

理論性的東西說了一堆钓试,難免有些瞌睡弓熏,先看一個使用的例子。

首先說明一下糠睡,我們的基礎框架中用到了 RxJava2挽鞠、Retrofit2

這里使用了 玩Android 的開放api接口進行測試,測試demo的目錄結構如下:

demo

首先看 WanAndroidApis接口狈孔,按照 Retrofit2 的要求信认,需要在里邊聲明網絡請求的接口:

public interface WanAndroidApis {
    String BASE_URL = Url.WAN_ANDROID_UTL;

    @GET("banner/json")
    Observable<BaseResponse<List<BannerBean>>> banner();

    @GET("friend/json")
    Observable<BaseResponse<List<FriendBean>>> friend();
}

bean 目錄對應接口返回 JSON 對應的類,就不多說了均抽。

SampleContract是一個契約接口嫁赏,這里約定了 Activity 需要實現(xiàn)的接口油挥,以及接下來 Presenter 類中需要實現(xiàn)的業(yè)務方法:

public interface SampleContract {
    interface View extends BaseView {
        void onBannerSuccess(List<BannerBean> data);

        void onBannerError(ResponseException e);

        void onFriendSuccess(List<FriendBean> data);

        void onFriendError(ResponseException e);
    }

    interface Presenter {
        void getBannerData();

        void getFriendData();
    }
}

SamplePresenterImpl類對應 MVP 中的 P潦蝇,繼承了我們基礎框架中封裝的BasePresenter類,同時實現(xiàn)了 SampleContract中的Presenter接口:

public class SamplePresenterImpl extends BasePresenter<SampleContract.View> implements SampleContract.Presenter {
    public SamplePresenterImpl(Context context, SampleContract.View view) {
        super(context, view);
    }

    @Override
    public void getBannerData() {
        RequestManager.getInstance().execute(this, RetrofitManager.getInstance().create(WanAndroidApis.class).banner(),
                new BaseObserver<List<BannerBean>>(context, true, true) {
                    @Override
                    protected void onSuccess(List<BannerBean> data) {
                        view.onBannerSuccess(data);
                    }

                    @Override
                    protected void onError(ResponseException e) {
                        view.onBannerError(e);
                    }
                });
    }

    @Override
    public void getFriendData() {
        RequestManager.getInstance().execute(this, RetrofitManager.getInstance().create(WanAndroidApis.class).friend(),
                new BaseObserver<List<FriendBean>>(true) {
                    @Override
                    protected void onSuccess(List<FriendBean> data) {
                        view.onFriendSuccess(data);
                    }

                    @Override
                    protected void onError(ResponseException e) {
                        view.onFriendError(e);
                    }
                });
    }
}

最后來看MainActivity深寥,繼承了我們自己封裝的基類BaseMvpActivity攘乒,同時實現(xiàn)了SampleContract中的View接口,并完成對應 Presenter 類的初始化:

public class MainActivity extends BaseMvpActivity<SamplePresenterImpl> implements SampleContract.View {

    // 初始化 Presenter 類
    @Override
    protected SamplePresenterImpl initPresenter() {
        return new SamplePresenterImpl(context, this);
    }

    //  通過 Presenter 發(fā)起初始化網絡請求
    @Override
    protected void loadData() {
        presenter.getBannerData();
        presenter.getFriendData();
    }
    // 設置布局文件 id
    @Override
    protected int initLayoutResID() {
        return R.layout.activity_main;
    }
    // 數(shù)據初始化
    @Override
    protected void initData() {

    }
    // 控件初始化
    @Override
    protected void initView() {

    }

    @Override
    public void onBannerSuccess(List<BannerBean> data) {
        Log.e("banner", "success");
    }

    @Override
    public void onBannerError(ResponseException e) {
        Log.e("banner", "error");
    }

    @Override
    public void onFriendSuccess(List<FriendBean> data) {
        Log.e("friend", "success");
    }

    @Override
    public void onFriendError(ResponseException e) {
        Log.e("friend", "error");
    }
}

到這里一個基本的使用就完成了惋鹅,在 Fragment 中的使用也是類似的持灰。解耦效果顯而易見,職責劃分更加清晰负饲。

其實核心的就是每個有 MVP 需求的 Activity 或 Fragment 需要定義一個 Contract 契約接口與之對應堤魁,然后再實現(xiàn)一個具體的 Presenter 類。

更多實現(xiàn)細節(jié)可參考:https://github.com/SheHuan/EasyMvp

四返十、MVP 的封裝過程

既然要封裝妥泉,我們就需要盡可能的把通用的代碼抽離出來,將一些常見的問題洞坑、需求在基礎代碼中處理好盲链。封裝后基礎框架目錄結構如下:


easymvp

1、Model 層

首先看 Model 層的封裝,Model 層主要封裝了基于 Retrofit2 的網絡請求刽沾,即RetrofitManager類:

public class RetrofitManager {
    private OkHttpClient okHttpClient;
    private RetrofitManager() {
    }

    public static RetrofitManager getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final RetrofitManager INSTANCE = new RetrofitManager();
    }

    /**
     * 根據不同的ApiService接口創(chuàng)建ApiService對象
     */
    public <S> S create(Class<S> service) {
        Retrofit retrofit = new Retrofit.Builder()
                .client(getOkHttpClient(true)) // 設置請求超時本慕、日志相關的攔截器
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl(getBaseUrl(service))
                .build();
        return retrofit.create(service);
    }

    /**
     * 解析接口中的BASE_URL,解決BASE_URL不一致的問題侧漓,如果所有的BASE_URL都一致锅尘,則可不用該方法
     */
    private <S> String getBaseUrl(Class<S> service) {

        try {
            Field field = service.getField("BASE_URL");
            return (String) field.get(service);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
.......
}

Retrofit2 和 RxJava2 是一對好搭檔,可以方便的幫助我們完成網絡請求的線程切換以及相應處理布蔗,所以RequestManager就是一個可以直接使用的網絡請求類:

public class RequestManager {
    private RequestManager() {
    }

    public static RequestManager getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final RequestManager INSTANCE = new RequestManager();
    }

    /**
     * 通用網絡請求方法
     */
    public <E> Disposable execute(BasePresenter presenter, Observable<BaseResponse<E>> observable, BaseObserver<E> observer) {
        observable
                .map(new ResponseConvert<E>())
                .onErrorResumeNext(new ExceptionConvert<E>())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(observer);
        presenter.addDisposable(observer.getDisposable());
        return observer.getDisposable();
    }

    /**
     * 通用耗時任務執(zhí)行方法
     */
    public <E> Disposable commonExecute(BasePresenter presenter, final ExecuteListener<E> listener, BaseObserver<E> observer) {
        ......
    }

    /**
     * 同時執(zhí)行兩個網絡請求藤违,統(tǒng)一處理請求結果
     */
    public <E1, E2, E3> Disposable zipExecute(BasePresenter presenter, Observable<BaseResponse<E1>> observable1, Observable<BaseResponse<E2>> observable2, final ZipExecuteListener<E1, E2, E3> listener, BaseObserver<E3> observer) {
        ......
    }

    /**
     * 依次執(zhí)行兩個網絡請求
     */
    public <E1, E2> Disposable orderExecute(BasePresenter presenter, Observable<BaseResponse<E1>> observable1, final OrderExecuteListener<E1, E2> listener, BaseObserver<E2> observer) {
        ......
    }
}

BaseObserver類相當于是網絡請求的觀察者類,可以用來監(jiān)聽網絡請求的開始纵揍、完成顿乒、發(fā)生錯誤等:

public abstract class BaseObserver<E> implements Observer<E> {
    private WeakReference<Context> wrContext;
    private Disposable disposable;
    private BaseNiceDialog dialog;
    // 發(fā)生異常時,是否顯示對應的 Toast 提示
    private boolean showErrorTip;

    /**
     * @param context      由于loading通過DialogFragment實現(xiàn)泽谨,無法使用Application Context璧榄,需要使用Activity Context
     * @param showLoading  是否顯示加載中l(wèi)oading
     * @param showErrorTip 發(fā)生異常時,是否使用Toast提示
     */
    public BaseObserver(Context context, boolean showLoading, boolean showErrorTip) {
        wrContext = new WeakReference<>(context);
        this.showErrorTip = showErrorTip;
        if (showLoading) {
            initLoading();
        }
    }

    public BaseObserver(boolean showErrorTip) {
        this.showErrorTip = showErrorTip;
        wrContext = new WeakReference<>(App.getContext());
    }

    @Override
    public void onSubscribe(Disposable d) {
        disposable = d;
        showLoading();
    }

    @Override
    public void onNext(E data) {
        hideLoading();
        onSuccess(data);
    }

    @Override
    public void onError(Throwable e) {
        hideLoading();
        ResponseException responseException = (ResponseException) e;
        if (showErrorTip) {
            Toast.makeText(wrContext.get(), responseException.getErrorMessage(), Toast.LENGTH_SHORT).show();
        }
        onError(responseException);
    }

    @Override
    public void onComplete() {

    }

    public Disposable getDisposable() {
        return disposable;
    }

    protected abstract void onSuccess(E data);

    protected abstract void onError(ResponseException e);

    /**
     * 初始化loading
     */
    private void initLoading() {
        dialog = NiceDialog.init()
                .setLayoutId(R.layout.loading_layout)
                .setWidth(100)
                .setHeight(100)
                .setDimAmount(0);
    }

    /**
     * 顯示loading
     */
    private void showLoading() {
        if (dialog != null) {
            dialog.show(((BaseActivity) wrContext.get()).getSupportFragmentManager());
        }
    }

    /**
     * 取消loading
     */
    private void hideLoading() {
        if (dialog != null) {
            dialog.dismiss();
            dialog = null;
        }
    }
}

在這里邊我們可以按需顯示耗時任務過程中的 Loading吧雹,以及異常提示 Toast犹菱。

RequestManager類還用到了一個ResponseConvert類,將BaseResponse響應根據接口約定的響應碼進一步處理

public class ResponseConvert<E> implements Function<BaseResponse<E>, E> {
    @Override
    public E apply(BaseResponse<E> baseResponse) {
        if (!"0".equals(baseResponse.getErrorCode())) {
            // 手動拋出異常
            throw new ApiException(baseResponse.getErrorCode(), baseResponse.getErrorMsg());
        }
        return baseResponse.getData();
    }
}

由于測試代碼使用了 玩Android 的api吮炕,對應的響應數(shù)據bean可以這樣定義:

public class BaseResponse<T>{
    private String errorCode;
    private String errorMsg;
    private T data;
    ......
}

BaseResponse類的前兩個字段需要根據自己接口的數(shù)據格式進行修改。

RequestManager類中還有一個ExceptionConvert類访得,當網絡請求過程中遇到異常時龙亲,不會直接拋出異常,而是進一步轉發(fā)異常信息悍抑,方便統(tǒng)一處理:

public class ExceptionConvert<E> implements Function<Throwable, ObservableSource<? extends E>> {
    @Override
    public ObservableSource<? extends E> apply(Throwable throwable) throws Exception {
        return Observable.error(ExceptionHandler.handle(throwable));
    }
}

ExceptionHandler類會對異常信息進行統(tǒng)一的轉換處理鳄炉,如果需要定制部分異常提示信息則可以著手修改該類。
到此 Model 層的核心功能就實現(xiàn)了搜骡,也是我們封裝過程中最復雜的部分拂盯。

2、Presente 層

接下來看 Presenter 層的實現(xiàn)记靡,就是一個BasePresenter類:

public abstract class BasePresenter<V extends BaseView> {
    protected V view;
    protected Context context;
    private CompositeDisposable compositeDisposable;

    public BasePresenter(Context context, V view) {
        this.context = context;
        this.view = view;
        compositeDisposable = new CompositeDisposable();
    }

    /**
     * 保存RxJava綁定關系
     */
    public void addDisposable(Disposable disposable) {
        if (!compositeDisposable.isDisposed()) {
            compositeDisposable.add(disposable);
        }
    }

    /**
     * 取消單個RxJava綁定
     */
    public void removeDisposable(Disposable disposable) {
        if (!compositeDisposable.isDisposed()) {
            compositeDisposable.remove(disposable);
        }
    }

    /**
     * 當Activity谈竿、Fragment destory時,取消當前Presenter的全部RxJava綁定摸吠,置空view
     */
    public void detach() {
        if (!compositeDisposable.isDisposed()) {
            compositeDisposable.clear();
        }
        view = null;
    }
}

核心的功能就是保存 RxJava 訂閱時返回的Disposable對象空凸,這一點在RequestManagerexecute方法中已經有所體現(xiàn)。還有就是取消 RxJava 訂閱關系寸痢,防止內存泄漏呀洲、發(fā)生異常。

3、View 層

Presenter類是在 View 層道逗,即基類 BaseMvpActivity兵罢、BaseMvpFragment中完成初始化以及DisposableView接口對象的釋放操作:

public abstract class BaseMvpActivity<P extends BasePresenter> extends BaseActivity {
    protected P presenter;

    // 初始化Presenter
    protected abstract P initPresenter();

    // 默認數(shù)據請求
    protected abstract void loadData();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        presenter = initPresenter();
        loadData();
    }

    @Override
    protected void onDestroy() {
        if (presenter != null) {
            presenter.detach();
        }
        super.onDestroy();
    }
}

View 層的實現(xiàn)相對簡單,BaseMvpFragment中我們實現(xiàn)了 Fragment 懶加載操作滓窍,在BaseActivity卖词、BaseFragment中默認初始化了 ButterKnife 方便控件的綁定。

大家應該還見過其它結構的 MVP 代碼贰您,因為 MVP 更是一種設計思想坏平,并沒有限制性的規(guī)定你的代碼必須要怎么寫,所以只要我們的思路正確锦亦,適合自己的封裝就是好的舶替。

更多實現(xiàn)細節(jié)可參考:https://github.com/SheHuan/EasyMvp

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市杠园,隨后出現(xiàn)的幾起案子顾瞪,更是在濱河造成了極大的恐慌,老刑警劉巖抛蚁,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陈醒,死亡現(xiàn)場離奇詭異,居然都是意外死亡瞧甩,警方通過查閱死者的電腦和手機钉跷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肚逸,“玉大人爷辙,你說我怎么就攤上這事‰伲” “怎么了膝晾?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長务冕。 經常有香客問我血当,道長,這世上最難降的妖魔是什么禀忆? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任臊旭,我火速辦了婚禮,結果婚禮上箩退,老公的妹妹穿的比我還像新娘巍扛。我一直安慰自己,他們只是感情好乏德,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布撤奸。 她就那樣靜靜地躺著吠昭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胧瓜。 梳的紋絲不亂的頭發(fā)上矢棚,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機與錄音府喳,去河邊找鬼蒲肋。 笑死,一個胖子當著我的面吹牛钝满,可吹牛的內容都是我干的兜粘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼弯蚜,長吁一口氣:“原來是場噩夢啊……” “哼孔轴!你這毒婦竟也來了?” 一聲冷哼從身側響起碎捺,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤路鹰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后收厨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晋柱,經...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年诵叁,在試婚紗的時候發(fā)現(xiàn)自己被綠了雁竞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拧额,死狀恐怖碑诉,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情势腮,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布漫仆,位于F島的核電站捎拯,受9級特大地震影響,放射性物質發(fā)生泄漏盲厌。R本人自食惡果不足惜署照,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吗浩。 院中可真熱鬧建芙,春花似錦、人聲如沸懂扼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至赶熟,卻和暖如春瑰妄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背映砖。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工间坐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留反肋,地道東北人兼雄。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓雅潭,卻偏偏與公主長得像葛假,于是被迫代替她去往敵國和親肾胯。 傳聞我的和親對象是個殘疾皇子真屯,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內容