Android---我所理解的MVP模式

前言

两年前第一次接触MVP模式,就被各种接口各种分层给弄的云里雾里。相信大多数的朋友第一次接触MVP的实例就是网上泛滥的那个登录模型,没错,就是这个没有任何卵用的模型让我认识到最初的MVP模式。随着这两年频繁的接触MVP,对这种质壁分离的模式也有了些小小的见解,下面就来分析一下。

模型简介

MVP模式简单的理解可以概括为将一个业务拆分成Model,View,Presenter三层结构。

Model层主管数据处理,包括网络数据,数据库数据,文件IO读写等;
View层主管UI更新和与UI相关的简单逻辑;
Presenter就是连接Model层和View层之间的桥梁,沟通Model和View进行各种交流,其作用一方面是获取Model层数据,二就是通过拿到的数据,进行部分逻辑处理后,交由View层进行UI更新。

这篇文章需要有一定的MVP基础,不然你可能会懵逼

乞丐版MVP

我这里将未经封装的MVP模式简称为乞丐版MVP,这种MVP只是简单的使用了MVP的思想,粗暴的写出来M,V,P三层结构,未经封装,存在大量的代码冗余和部分内存泄漏的风险,所以乞丐版MVP只是用来理解MVP模式,不能直接放在项目中使用。



简单模拟一个场景:假设在一个Activity中有一个网络请求,拿到网络数据后在P层进行相关逻辑处理后,在V层进行相应数据更新。
我们从M层先写,一步一步倒追到V层,首先M层有一个网络请求:

public class DemoModel {
    public void onRequestData(final CallBack<DemoBean> callBack) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //模拟网络请求
                SystemClock.sleep(5000);
                //网络请求成功
                callBack.onSuccess(new DemoBean("哈哈"));
            }
        }).start();
    }
}

根据MVP的思想,V层和M层不能有直接的交流,那么肯定就是P层来进行调用M层的代码:

public class DemoPresenter {

    private IDemoView mView;
    private DemoModel mModel;

    public DemoPresenter(IDemoView view) {
        mView = view;
        mModel = new DemoModel();
    }


    public void onRequestData() {
        mModel.onRequestData(new CallBack<DemoBean>() {
            @Override
            public void onSuccess(DemoBean data) {
                mView.updateUI(data);
            }
        });
    }
}

V层和P层直接是通过接口的形式IDemoView进行交互,当然P层和M层也可以使用接口交互:

public interface IDemoView {

    void updateUI(DemoBean data);
}

V层也就是相当于我们的Activity:

public class DemoActivity extends AppCompatActivity implements IDemoView{

    private DemoPresenter mPresenter = new DemoPresenter(this);

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter.onRequestData();
    }

    @Override
    public void updateUI(DemoBean data) {
        Toast.makeText(this, data.str, Toast.LENGTH_SHORT).show();
    }
}

这就是一个最基本的MVP结构,V层和M层阻隔,P作为中枢系统,将M层拿到的数据通过一定的逻辑处理后,交由V层进行UI更新。
乞丐版MVP有以下几个缺点:

1.V中使用P都需要new一个出来,不同的V使用的P不同,但创建形式相同,代码存在冗余;
2.P中使用M也是在构造器中new一个M,M作为网络请求,可能会存在多种场景复用的情况,这种写法阻碍了代码的复用;
3.V层被干掉,P层却没有收到相应关闭的通知,存在一定的内存泄漏;

封装后的MVP框架

针对于乞丐版MVP,这里我做了以下的几点封装:

1.创建V,P,M层对应的基类,如:BaseMVPActivity,BasePresenter,BaseModel;
2.使用注解的形式,在BaseMVPActivity中自动创建出我们所需要的P,减少代码冗余;
3.创建一个ModelManager管理类,使用反射的形式创建出Model,减少P和M之间的耦合,增强复用性;
4.P层中增加其生命周期方法,绑定于对应的V层,V层被干掉的情况下,保证P也会结束掉相应的工作,减少内存泄漏;
5.使用Loader自动管理P,防止在屏幕状态改变的情况下,创建多个P。

先看一个结构图:
MVP框架流程图.jpg

结构图上可以清晰的看到调用情况,V层通过注解的形式创建P,P通过IBaseView通知V,P通过Token类加载ModelManager获取对应的M对象,ModelManager通过CallBack回调来通知P拿取数据,ModelManager使用反射的形式管理众多M,这样就成功的将M层单独抽离成一个大模块。

废话不多说,首先先看下使用情况:
V层:

@CreatePresenter(MainPresenter.class)
public class MainActivity extends BaseMVPActivity<MainPresenter> implements IMainView {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onPresenterCreate() {
        mPresenter.show();
    }

    @Override
    public void show() {
        Log.d("MainActivity", "呵呵");
    }
}

P层:

public class MainPresenter extends BasePresenter<IMainView> {

    public void show() {
        HttpParam param = HttpParam.obtain();
        param.put(APIParamKey.PHONE, "13112345678");

        ModelManager.getModel(Token.MAIN_MODE)
                .setParam(param)
                .execute(new CallBack<MainBean>() {
                    @Override
                    public void onSuccess(MainBean data) {
                           mView.show();
                    }

                    @Override
                    public void onFailure(String msg) {

                    }
                });
    }
}

M层:

public class MainModel extends BaseModel<MainBean> {

    @Override
    public void execute(final CallBack<MainBean> callBack) {
        super.execute(callBack);
        HttpParam param = getParam();
        String phone = param.get(APIParamKey.PHONE);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                callBack.onSuccess(new MainBean());
            }
        }, 4000);
    }
}

封装后的结构比之前要清晰许多,代码也少了一部分,M层和P层也实现了完全的解耦合。
那么我们来先看看M层和P层是怎么样实现完全的解耦合的呢:

M层主要封装:

ModelManager:

public class ModelManager {

    private static ArrayMap<String, IBaseModel> mCache = new ArrayMap<>();

    /**
     * 传递类,反射获得该类对象
     *
     * @param token 类引用字符串
     */
    public static IBaseModel getModel(Class<? extends BaseModel> token) {
        return getModel(token.getName());
    }

    /**
     * 传递类引用字符串,反射获得该类对象
     *
     * @param token 类引用字符串
     */
    public static IBaseModel getModel(String token) {
        IBaseModel model = mCache.get(token);
        try {
            if (model == null) {
                model = (IBaseModel) Class.forName(token).newInstance();
                mCache.put(token, model);
            }
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return model;
    }
}

主要是使用反射来获取对应的Model类,增加mCache缓存实现容器类单例。这里我实现了两个方法,一个是传递Class类,一个是传递字符串。我个人建议是使用传递字符串的方式来创建Model,通过一个类Token,封装所有的Model信息,使P和M直接只通过ModelManager的形式来交互,也便于M的复用。

ModelManager第二个方法是传递一个类的引用字符串,我把所有要引用的字符串写到一个类里,便于管理,起名叫Token:

public interface Token {
    //    String MAIN_MODE = "com.zdu.mvpdemo.MainModel";
    String MAIN_MODE = MainModel.class.getName();
}

我写了两种方式来引用字符串,推荐使用第二种,使用XXXModel.class.getName()的方式来获取当前类的引用。让Token和各个Model直接建立起联系,便于查找,同时也能防止混淆时Model类因为没有任何引用而被认为是无效类从而被删掉的问题。

BaseModel类中主要实现了4个方法:

参数的set和get方法:

 @Override
    public BaseModel<T> setParam(HttpParam param) {
        this.mParam = param;
        mParam.setUse(true);
        return this;
    }

    @Override
    public HttpParam getParam() {
        if (mParam == null) {
            throw new NullPointerException("入参为空");
        }
        return mParam;
    }

HttpParam类是我封装的一个ArrayMap集合,内部加入了缓冲池的概念。优点就是可以重复利用,缺点就是无法动态的指定Map集合的大小。至于怎么实现的缓存池,请看我写的另一篇文章: Android 模拟Message.obtain(),构建自己的缓存池

两个执行方法 excute(CallBack<T>),和空参excute()

/**
     * 执行异步操作
     *
     * @param callBack<T> 回调
     */
    @Override
    public void execute(@Nullable CallBack<T> callBack) {
        recycleParam();
    }

    /**
     * 执行异步操作,无需回调
     */
    @Override
    public void execute() {
        execute(null);
    }
   
    /**
     * 回收
     */
    private void recycleParam() {
        if (mParam != null) {
            mParam.setUse(false);
            mParam.recycle();
        }
    }

两个方法我都没设置成抽象的,简单实现了下回收的方法,一个带回调,一个不带回调。子类想调用哪个就实现哪个方法就可以了

V层主要封装:

V层主要实现BaseMVPActivity,封装了以下几个功能:

基类自动与P层进行关联,并进行生命周期绑定
注解@CreatePresenter的形式自动创建P
使用Loader管理P的加载,防止屏幕状态改变时,创建多个P

BaseMVPActivity:

public class BaseMVPActivity<P extends BasePresenter> extends BaseActivity implements LoaderManager.LoaderCallbacks<P>, IBaseView {

    private final int LOADER_ID = TAG.hashCode();
    protected P mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportLoaderManager().initLoader(LOADER_ID, savedInstanceState, this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mPresenter != null) {
            mPresenter.onPause();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mPresenter != null) {
            mPresenter.onResume();
        }
    }

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

    @NonNull
    @Override
    public Loader<P> onCreateLoader(int id, @Nullable Bundle args) {
        return new PresenterLoader<>(this, getClass());
    }

    @Override
    public void onLoadFinished(@NonNull Loader<P> loader, P data) {
        if (data != null) {
            mPresenter = data;
            mPresenter.onAttach(this);
            onPresenterCreate();
        }
    }

    @Override
    public void onLoaderReset(@NonNull Loader<P> loader) {
        if (mPresenter != null) {
            mPresenter.onDetach();
        }
        mPresenter = null;
    }

    /**
     * 回调此方法,presenter创建完毕
     */
    protected void onPresenterCreate() {

    }

}

首先在onCreate方法初始化Loader,在实现的onCreateLoader方法里加载注解解析器PresenterLoader,加载完成实现方法onLoadFinished进行P的赋值和接口的绑定,数据被重置时实现onLoaderReset,接触对P的绑定,对应的生命周期方法里也将P绑定在一起。

注解解析器PresenterLoader:

public class PresenterLoader<P extends BasePresenter> extends Loader<P> {

    private CreatePresenter mCreatePresenter;
    private P mPresenter;

    public PresenterLoader(Context context, Class<?> tClass) {
        super(context);
        mCreatePresenter = tClass.getAnnotation(CreatePresenter.class);
    }

    private P getPresenter() {
        if (mCreatePresenter != null) {
            Class<P> pClass = (Class<P>) mCreatePresenter.value();
            try {
                return pClass.newInstance();
            } catch (Exception e) {
                throw new RuntimeException("Presenter创建失败!,检查是否声明了@CreatePresenter(xx.class)注解");
            }
        }
        return null;
    }

    @Override
    protected void onStartLoading() {
        super.onStartLoading();
        if (mPresenter == null) {
            forceLoad();
        } else {
            deliverResult(mPresenter);
        }
    }

    @Override
    protected void onForceLoad() {
        super.onForceLoad();
        mPresenter = getPresenter();
        deliverResult(mPresenter);
    }
}

这个类的作用其一就是绑定Loader,管理P的创建加载情况,其二就是获取注解对象,通过反射来创建P。

注解类就很简单了:

@Retention(RetentionPolicy.RUNTIME)
public @interface CreatePresenter {

    Class<? extends BasePresenter> value();
}

有一定的约束条件,必须继承BasePresenter才能通过注解来创建。

P层的封装:

V和M都搞定了,作为桥梁的P层就简单了:
BasePresenter:

public class BasePresenter<V extends IBaseView> implements IBasePresenter<V> {

    protected V mView;

    @Override
    public void onAttach(V view) {
        mView = view;
    }

    @Override
    public V getView() {
        checkViewAttached();
        return mView;
    }

    @Override
    public void onPause() {
    }

    @Override
    public void onResume() {
    }

    @Override
    public boolean isViewAttached() {
        return mView != null;
    }

    @Override
    public void checkViewAttached() {
        if (!isViewAttached()) {
            throw new NullPointerException("View 已为空");
        }
    }

    /**
     * 取消,置空数据,防止内存泄露
     */
    @Override
    public void onDetach() {
        mView = null;
    }

}

自动装箱:onAttach(),自动拆箱:onDetach(),还有各种生命周期,逻辑处理起来就方便了很多。

综述

通过一定的封装,创建P的过程交给基类通过注解的形式来创建,减少了代码的冗余;给P一定的生命周期方法,在V被干掉的情况下,P能及时处理对应的事件,减少内存泄漏;通过ModelManager统一管理Model,使用Token类作为Model和ModelManager的桥梁,彻底的将P和M进行解耦,同时分离了M层,使M层抽出成一个独立的模块,增加了M的复用性。
附上Demo的GitHub地址:MVPDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 昨天和路兄夜出观影《一出好戏》,甚悦。舒淇美甚。 8月16日未更新。以下是今天更新的内容: 一个不被期待,不被鼓励...
    小_富_贵阅读 192评论 0 0
  • 有时候提笔写字会跟心里想的是不太一致的,我很是认为自己并没有表达出我的情感来,任何一个人在当众剖析自己时,那面临的...
    郑重承诺阅读 273评论 0 1
  • 文人轩记 应秦皇之邀,东西南北中和骊宫,聚华清园下。兴瀑泉以温身心,居静阁以求灵魂。处少阳之室内,行颐和之...
    邓风来阅读 275评论 0 3
  • 相信每一个女孩纸都萌生过开一家咖啡店或甜品店或者书店的想法,我也是……我曾多次幻想将来能够与魔都的某一优雅之地开一...
    Surquee阅读 107评论 0 1