Android 实现一个 MVP 基础框架

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

这几年 MVP 在 Android 开发中已经开始被广泛使用,逐渐成为一种主流的设计思想。在 MVP 出现之前,我们使用最多的可能就是 MVC 了,那么我们为什么要使用 MVP,它解决了 MVC 使用中的那些痛点呢,那我们先从 MVC 说起。

一、浅谈 MVC

MVC 的全称是 Model-View-Controller,这三部分在 Android 中可以按照如下的层次划分:

  • Model(数据模型层)
    主要负责网络请求、数据库等其它 I/O 操作
  • View(视图层)
    主要包括 XML 布局文件(但功能较弱)
  • Controller(控制器层)
    一般就是 Activity 或者 Fragment,负责 MVC 整个流程的调度、协作。但是由于 XML 的视图处理能力较弱,需要 Activity 或者 Fragment 承担部分视图层工作。

在 Android 中可以用网络请求的例子来梳理一下 MVC 的流程,例如,在 Activity(Controller)中通过一个 Button 点击事件调用 Model 层代码发起网络请求,当 Model 层网络请求成功响应后,在对应的回调函数中去更新 View 层的一个 RecyclerView。按照事件的流向,例子中 MVC 三部分的关系如下:

MVC

你可能还见过不同的 MVC 关系图,其实都是对的,因为不同的 MVC 实现可能会有不同的事件流向,选择适合自己的就行。

上图看起来层次结构很清晰,但还是存在一些问题的,虽然我们可以把网络请求的 Model 层解耦成一个单独的模块,但从网络请求的发起到响应结果的处理都集中在了 Activity 中,View 层的功能基本被弱化的可以忽略了,基本由 Activity 承担了,所以 Activity 同时承担了 Controller 和 View 层的职责,带来了耦合的问题,随着业务量的增加必然会导致 Activity 变的臃肿起来,增加维护成本。

那么面对这些问题 MVP 是如何应对的呢,它和 MVC 有何异同呢......

二、更好的 MVP

MVP 的全称是 Model-View-Presenter,它保留了 MVC 中的 View 层和 Model 层,用 Presenter 层替代了 Controller 层,在 Android 开发中三个层次的职责划分如下:

  • Model(数据模型层)
    主要负责网络请求、数据库等其它 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 层的实现、调用对 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 需要实现的接口,以及接下来 Presenter 类中需要实现的业务方法:

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类,同时实现了 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,同时实现了SampleContract中的View接口,并完成对应 Presenter 类的初始化:

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

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

    //  通过 Presenter 发起初始化网络请求
    @Override
    protected void loadData() {
        presenter.getBannerData();
        presenter.getFriendData();
    }
    // 设置布局文件 id
    @Override
    protected int initLayoutResID() {
        return R.layout.activity_main;
    }
    // 数据初始化
    @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 契约接口与之对应,然后再实现一个具体的 Presenter 类。

更多实现细节可参考: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接口创建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();
    }

    /**
     * 通用耗时任务执行方法
     */
    public <E> Disposable commonExecute(BasePresenter presenter, final ExecuteListener<E> listener, BaseObserver<E> observer) {
        ......
    }

    /**
     * 同时执行两个网络请求,统一处理请求结果
     */
    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) {
        ......
    }

    /**
     * 依次执行两个网络请求
     */
    public <E1, E2> Disposable orderExecute(BasePresenter presenter, Observable<BaseResponse<E1>> observable1, final OrderExecuteListener<E1, E2> listener, BaseObserver<E2> observer) {
        ......
    }
}

BaseObserver类相当于是网络请求的观察者类,可以用来监听网络请求的开始、完成、发生错误等:

public abstract class BaseObserver<E> implements Observer<E> {
    private WeakReference<Context> wrContext;
    private Disposable disposable;
    private BaseNiceDialog dialog;
    // 发生异常时,是否显示对应的 Toast 提示
    private boolean showErrorTip;

    /**
     * @param context      由于loading通过DialogFragment实现,无法使用Application Context,需要使用Activity Context
     * @param showLoading  是否显示加载中loading
     * @param showErrorTip 发生异常时,是否使用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,对应的响应数据bean可以这样定义:

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

BaseResponse类的前两个字段需要根据自己接口的数据格式进行修改。

RequestManager类中还有一个ExceptionConvert类,当网络请求过程中遇到异常时,不会直接抛出异常,而是进一步转发异常信息,方便统一处理:

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类会对异常信息进行统一的转换处理,如果需要定制部分异常提示信息则可以着手修改该类。
到此 Model 层的核心功能就实现了,也是我们封装过程中最复杂的部分。

2、Presente 层

接下来看 Presenter 层的实现,就是一个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方法中已经有所体现。还有就是取消 RxJava 订阅关系,防止内存泄漏、发生异常。

3、View 层

Presenter类是在 View 层,即基类 BaseMvpActivityBaseMvpFragment中完成初始化以及DisposableView接口对象的释放操作:

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

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

    // 默认数据请求
    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 层的实现相对简单,BaseMvpFragment中我们实现了 Fragment 懒加载操作,在BaseActivityBaseFragment中默认初始化了 ButterKnife 方便控件的绑定。

大家应该还见过其它结构的 MVP 代码,因为 MVP 更是一种设计思想,并没有限制性的规定你的代码必须要怎么写,所以只要我们的思路正确,适合自己的封装就是好的。

更多实现细节可参考:https://github.com/SheHuan/EasyMvp

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容