Android MVP与MVC

一、MVP与MVC介绍

先上一个经典的图:


C 和 P 的区别

先来看一下 MVP MVC 差别在哪?简单一眼扫过,就是C P 的差别。

1、先看 C

C 就是 Controller,控制器。负责从View 读取数据,控制用户输入,并向Model 发送数据。简单来说,就是起到一个沟通的作用,能很大程度上的解决Model View 的耦合问题。

换句话说就是,它是一个 Model View 之间的桥梁,让Model View 之间不再紧紧关联。

比如 View 接收到了用户输入数据,先交给Controller Controller 再转交给Model ,反之亦然。

这就像小明喜欢隔壁班小红,小明写了一封情书需要通过隔壁班小王,才能交给小红。

扫描二维码关注公众号,回复: 2600170 查看本文章

但是注意,我只是说能很大程度上解决,并不能彻底解决,也就是说小明如果发现了隔壁小王有问题,他仍然可以选择直接把情书交给小红。

2、再看 P

P 就是 Presenter,我翻译成主持者。跟C 类似,仍然是负责View Model 之间的沟通。但是它彻底让View Model 不能直接沟通。如果想要沟通,就必须通过这个主持者来主持它们两个应该干啥。

比如 View 接收到了用户输入数据,不能直接给Model ,要交给Presenter Presenter 再转交给Model ,反之亦然。

这就像我给主席寄了一个包裹,但这个包裹必须经过重重安检,才能交到主席手上。

这就彻底断了我跟主席……哦不对,Model View 之间的联系。

3、简单区别

仅从目前来看, C P 都是为了解放Model View 之间的联系,只不过C 是很大程度上解决,但P 是彻底让它们两断了联系。

换成技术术语来说就是一句话:

C  Model View 做到松散耦合,而P 直接将它们解耦

MVC 和 MVP 的区别

知道了各自简单的作用,再来更深层次的理解 C P 在各自的MV+X 中到底分别做了什么?

1、先看 MVC

从下图中我们可以看到:


用户 Event(事件)会导致Controller 改变Model View 或同时改变两者。

只要 Controller 改变了Model 的数据或属性,所有依赖的View 都会自动更新。

类似的,只要 Controller 改变了View View 会从潜在的Model 中获取数据进行更新。

2、再看 MVP

从下图中我们又能看到:


Presenter 中同时持有 View 以及Model Interface 引用,而View 持有Presenter 的实例。

当某个 View 需要展示某些数据时,首先会调用Presenter 的某个接口,然后Presenter 会调用Model 请求数据。

Model 数据加载成功后会调用Presenter 的回调方法通知Presenter 数据加载完毕,最后Presenter 再调用View 层接口展示加载后数据。

3、主要区别

MVC 中:

1)View 可以与 Model 直接交互;

2)Controller 可以被多个 View 共享;

3)Controller 可以决定显示哪个 View

MVP 中:

1)View 不直接与 Model 交互;

2)Presenter  View 通过接口来交互,更有利于添加单元测试;

3)通常 View Presenter 是一对一的,但复杂的View 可能绑定多个 Presenter 来处理;

4)Presenter 也可以直接进行 View 上的渲染。

二、经典案例

登录功能

先分析

好了,动手之前先分析一下。

从上面内容我们知道,Presenter 是用来Model View 之间交互的。所以必须要持有它们各自的对象,根据需求一般都是用接口来实现。

而实现 View 层接口的一般都是Activity

当然如果想要 Activity Model 进行交互,那么这个Activity 中还必须有一个Presenter 的实例,因为需要这个Presenter 来进行交互嘛!

OK,把上面所有的东西捋一捋,数一数到底需要啥:

1)Model:负责存储、检索、操纵数据,一般都会一些封装对数据比如 Bean的操作。

2)ModelInterface:这个不是必须的,但有时候如果几个 Bean之间有共性,可以抽一个接口出来。

3)View:暂且就认为是 Activity

4)ViewInterfaceView 需要实现的接口,View Presenter也是通过它来进行交互。

5)Presenter:最重要的 View Model 的桥梁,处理与用户交互的负责逻辑,需要持有ViewModel的接口对象。

虽然看起来东西确实变多了,但是结构看起来还是很清晰的,扩展起来也比较方便。

再动手

按照上面需要的东西,一步一步来:

1、先建一个 Bean

public class PersonBean {
    private String name ;
    private String pwd;
    //...省略
}

2、再建立 Model Interface

针对这个 Bean ,有注册和登录的功能,这里强行抽取一个IPersonModel接口出来,纯属为了展示用,意义不大:

public interface IPersonModel {
    //注册账号
    boolean onRegister(String name, String pwd);
    //登录账号
    boolean onLogin(String name, String pwd);
}

3、其次建立 Model  

实现了上一步建立的 Model Interface ,主要是对注册和登录方法的实现:

public class PersonModel implements IPersonModel {

    //简单的存一下注册的账号
    private Map<String, String> personMap = new HashMap<>();

    /**
     * 注册账号 存入集合
     *
     * @param name 用户名
     * @param pwd  密码
     * @return true:注册成功,false:注册失败
     */
    @Override
    public boolean onRegister(String name, String pwd) {
        if (!personMap.containsKey(name)) {
            personMap.put(name, pwd);
            return true;
        }
        return false;
    }

    /**
     * 登录账号
     *
     * @param name 用户名
     * @param pwd  密码
     * @return true:登录成功,false:登录失败
     */
    @Override
    public boolean onLogin(String name, String pwd) {
        return pwd.equals(personMap.get(name));
    }
}

4、还需要 View Interface 

在这里我设定了五个方法,其中注册/登录成功与否分别建了两个方法

public interface IPersonView {
    boolean checkInputInfo();  //检查输入的合法性
    void onRegisterSucceed();  //注册成功 
    void onRegisterFaild();    //注册失败 
    void onLoginSucceed();     //登录成功
    void onLoginFaild();       //登录失败
}

5、最重要的 Presenter 

再次强调,Presenter 是用来 Model  View 交互的,而它们各自都实现了接口,那我们只需保证Presenter 持有这些接口即可:

public class PersonPresenter {

    private IPersonModel mPersonModel;  //Model接口
    private IPersonView mPersonView;    //View接口

    public PersonPresenter(IPersonView mPersonView) {
        mPersonModel = new PersonModel();
        this.mPersonView = mPersonView;
    }

    public void registerPerson(String name, String pwd) {
        boolean isRegister = mPersonModel.onRegister(name, pwd);
        //根据Model中的结果调用不同的方法进行UI展示
        if(isRegister){
            mPersonView.onRegisterSucceed();
        }else{
            mPersonView.onRegisterFaild();
        }
    }

    public void loginPerson(String name, String pwd) {
        boolean isLogin = mPersonModel.onLogin(name, pwd);
        //根据Model中的结果调用不同的方法进行UI展示
        if (isLogin) {
            mPersonView.onLoginSucceed();
        }else{
            mPersonView.onLoginFaild();
        }
    }
}

6、最后的 View 

这里的 View 其实就是实现 IPersonView 接口的Activity,它必须有一个Presenter 的实例才能与Model 交互:

public class MainActivity extends AppCompatActivity implements IPersonView, View.OnClickListener {

    /*===== 数据相关 =====*/
    private PersonPresenter personPersenter;

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

        initView();     //初始化View
        initData();     //初始化Data
    }

    /**
     * 初始化Data
     */
    private void initData() {
        //持有 Presenter 实例
        personPersenter = new PersonPresenter(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt_main_register:
                if (checkInputInfo()) {
                    personPersenter.registerPerson(inputName, inputPwd);
                }
                break;
            case R.id.bt_main_login:
                if (checkInputInfo()) {
                    personPersenter.loginPerson(inputName, inputPwd);
                }
                break;
        }
    }

    /*========== IPersonView接口方法 START ==========*/

    /**
     * 检查输入信息的合法性
     *
     * @return true:输入合法,false:输入不合法
     */
    @Override
    public boolean checkInputInfo() {
        inputName = nameEText.getText().toString().trim();
        inputPwd = pwdEText.getText().toString().trim();

        if (inputName.equals("")) {
            nameEText.setError("用户名不能为空");
            return false;
        }
        if (inputPwd.equals("")) {
            pwdEText.setError("密码不能为空");
            return false;
        }
        return true;
    }

    @Override
    public void onRegisterSucceed() {
        showToast("注册成功");
    }

    @Override
    public void onRegisterFaild() {
        showToast("用户已存在");
    }

    @Override
    public void onLoginSucceed() {
        showToast("登录成功");
    }

    @Override
    public void onLoginFaild() {
        showToast("用户不存在或密码错误");
    }

    /*========== IPersonView接口方法 END ==========*/
}

三、Q & A

这里是一些疑问和解答:

Q MVP 模式中View 层是否就是Activity ?

A 其实严格意义上来说,这么说是不对的。虽然本例中确实是Activity ,但是在真正的项目中,需要考虑Activity  Fragment 的情况,甚至还要考虑一些特定的View 或者ViewGroup 

注:后面用 Activity 统一指代 View 了。

Q 从例子上看,几乎每一个Activity 都对应着一个 Presenter ,还需要其他的接口,那如果Activity 很多怎么办?

A 其实这个问题一直是MVP 饱受诟病的地方,虽然MVP 结构很清晰,但确实要增加很多很多的类,所以需要尽量让接口能适用于多种View ,但如果实在忍受不了,建议不用MVP

Q 使用MVP 后感觉项目更加臃肿和复杂了怎么办?

A 从来都没有人说过MVP 能使得项目简单,只是它会让项目结构更加清晰更加易于扩展而已。就像RxJava 一样,代码量还是那么多,但是流程更加清晰了,这就是能让开发者拥护的原因。

Q 为什么案例中IPersonView 这个接口将注册登录成功与否分开成独立方法?

A 这里确实可以不分开,只要将注册/登录的结果作为参数即可,但是这样的话,我们仍然需要在Activity 中根据结果参数来决定显示的Toast 内容。

也就是说 View 仍然需要处理一些来自Model 的逻辑,这样不是太符合MVP 的意义。所以将判断逻辑放在Presenter 中处理,View 层只管展示就行了。

包括鸿洋大神的那篇文章中,有一个 View 的方法直接传递了涉及Model 层的类,显然违背了MVP 的定义,我觉得不是太好(批判了大神,果断逃……)。

Q Presenter 如果进行耗时操作,但此时对应的Activity 被杀死,会报空指针么?

A 其实在这种情况下,已经存在内存泄漏的情况了。但有意思的是,并不会报空指针,具体原因暂时还不是特别清楚,可能回收的时候并没有完全回收,因为系统会认为还存在相关的引用,所以不会空指针。

Q 那该如何避免内存泄漏这种情况呢?

A 这个问题我看的时候觉得很简单,后来发现这是很有趣的问题。具体方法有很多,也有很多的开源库专门处理这样的问题。其实解决办法归纳起来就是一个如何让 Presenter的生命周期跟 Activity的生命周期保持一致

我看了很多方法,只觉得通过 Loader 的方法来解决是最简单也最有效的方法。

Presenter与手机状态

开发者们对于MVP的核心概念与整体实现基本达成了共识,但有关一些比较细节的问题仍存在争论。而这篇文章所讨论的问题就是当手机的状态发生改变时(比如旋转手机)如何处理Presenter对象。一般有如下几种方案:

就让Presenter一起挂掉吧

在这种方案下当Activity/Fragment挂掉时Presenter也一起挂掉了。此时如果想要保存一些状态信息的话就要借助onSaveInstanceState()方法了,需要在View的实现类中保存状态并在重新onCreate()时利用状态信息重新实例化Presenter,或者直接把保存下来的instance Bundle直接传给Presenter以便Presenter处理。如果是纯数据的话这样也没什么不好的,但如果要保存一些后台线程的引用的话就很难办了。

Presenter保存在一个地方,再次onCreate时还原

我见到的最多的就是这个方案,不过具体的实现方法千差万别。

最简单最naive的实现方式,就是在Activity/Fragment中保留对Presenter的静态引用。这样我们可以再手机旋转时保留Presenter对象,但整个Applicaiton中只能有一个同样的Activity/Fragment-Presenter实例组合。但当一个ViewPager中有多个相同的Fragment时,这种方法就行不通了。

另一个方式就是调用FragmentsetRetainInstance(true)方法。设置Fragment的这个属性可以保证Fragment不会被destroy,这样Presenter就随之被保留。但这种方式仍有局限,对一个子Fragment或是Activity这样就不起作用。

最后一种方式就是使用单例缓存机制并通过标识符来存储一个Presenter类的不同实例。为了保留PresenterActivity/Fragment需要在onSaveInstanceState()中传递Presenter实例的标识符。这里的问题是如何实现这种逻辑以及何时将Presenter从单例缓存中移除。

什么是Loader?它有什么用?

我们都知道,当手机状态发生改变比如旋转时,Activity会重新启动。LoaderAndroid框架中提供的在手机状态改变时不会被销毁的工具。Loader的生命周期是是由系统控制的,只有在向Loader请求数据的Activity/Fragment被永久销毁时才会被清除,所以也不需要自己写代码来清空它。

一般Loader是用来在后台加载数据的,而且是用它的子类CursorLoaderAsyncTaskLoader,尤其是CursorLoader,直接就绑定了Content Provider和数据库。当然如果写个类继承Loader基类的话也不需要开启后台线程。

听起来好厉害。但这和Presenter有什么关系?

就像刚才说的一样,关键问题就是在哪里存储Presenter以及什么时候销毁它们。而我们刚刚就看到了Loader的强大之处:由安卓系统框架提供,有单独生命周期,会被自动回收且不必在后台运行。

所以思考一下需求以及Loader的功能,我们可以让Loader作为Presenter的提供者,而不需要担心手机状态改变。

将同步的Loader作为存放Presenter的缓存。

这里的重点就在于同步使用Loader时,我们可以知道在生命周期的哪个阶段Presenter被创建了并且可以工作了。甚至是在Activity/Fragment可见之前。

要注意的是,ActivityFragment在何时传递Presenter对象这个问题上是有区别的。对于任何一个Activity实例,只需要在调用super.onStart()之后就可以使用Presenter了,但对于Fragment实例来说,首次创建时可以在super.onStart()之后传入,但在Fragment被重新create时,就必须要在super.onResume()之后传入了。所以在Fragment中,只需要在onResume()方法执行后将Presenter传入就行了。

使这种方法可行的另外一个要点就是系统对Loader的处理方式,每一个Activity/Fragment都有一个LoaderManager,而且只有这个LoaderManager可以管理与Activity/Fragment相关联的Loader,这就使得相同的Fragment与其Presenter可以同时存在多个实例。

alk is Cheap, Show Me the Code 废话少说,放码过来

首先我们要写一个继承Loader类的子类。由于我们不想让代码在后台线程运行,所以在这里不继承CursorLoaderAsyncTaskLoader。代码大概就是这样:

public class PresenterLoader<T extends Presenter> extends Loader<T>{
    private final PresenterFactory<T> factory;
    private T presenter;
 
    // 省略构造方法
 
    @Override
    protected void onStartLoading() {
 
        // 如果已经有Presenter实例那就直接返回
        if (presenter != null) {
            deliverResult(presenter);
            return;
            }
 
        // 如果没有
        forceLoad();
    }
 
    @Override
    protected void onForceLoad() {
        // 通过工厂来实例化Presenter
        presenter = factory.create();
 
        // 返回Presenter
        deliverResult(presenter);
    }
 
    @Override
    protected void onReset() {
        presenter.onDestroyed();
        presenter = null;
    }
}


在这个例子中Presenter的接口如下:

public interface Presenter<V>{
    void onViewAttached(V view);
    void onViewDetached();
    void onDestroyed();
}


解释一下核心代码:

onStartLoading():会在ActivityonStart()调用之后被系统调用来获取一个Loader实例。在这里先判断是否已经有Presenter对象了还是需要创建。

onForceLoad():在调用forceLoad()方法后自动调用,我们在这个方法中创建Presenter并返回它。

deliverResult():会将Presenter传递给Activity/Fragment

onReset():会在Loader被销毁之前调用,我们可以在这里告知Presenter以终止某些操作或进行清理工作。

presenterFactory:这个接口可以隐藏创建Presenter所需要的参数。通过这个接口我们可以调用各种构造器,这样可以避免写一堆PresenterLoader的子类来返回不同类型的Presenter。这个接口形式上大概就是这样:

  public interface PresenterFactory<T extends Presenter> {

      T create();

  }   

Activity/Fragment中如何实现呢?

现在我们已经实现了Loader,下面要将LoaderActivity/Fragment连接起来,刚才说过,这个连接点就是LoaderManager。我们需要调用FragmentActivitygetSupportLoaderManager()FragmentgetLoaderManager()方法来获得LoaderManager实例并调用其initLoader()方法。Google建议在ActivityonCreate()FragmentonActivityCreated()中调用此方法。

当调用initLoader()方法时要传入一个id,只需要保证在一个Activity/Fragment内单一即可,不需要全局单一。这个id就是用来识别Loader的。还可以选择传入一个Bundle,但在这个例子中不需要。还要穿入一个LoaderCallbacks实例。

刚才说过,不要再FragmentonCreate()方法中调用initLoader()方法,要在onActivityCreated()中调用它,不然就会遇到不同Fragment共享一个Loader的问题。

如果你发现在手机状态改变时onLoadFinished()会被调用两次,不妨参考stackoverflow下的这个问题Presenter在被传入Activity后的逻辑可能会使这个成为一个问题,此时可以尝试在FragmentonResume()方法中调用initLoader()方法,或者在onActivityCreate()方法中存一个flag以防多次获取Presenter

当我们调用了initLoader()LoaderActivity/Fragment的生命周期就绑定了:执行onStart()方法时会调用onStartLoading(),执行onStop()方法时会调用onStopLoading()。但onReset()方法只会在Activity/Fragment被销毁或主动调用destroyLoader()时被调用。

LoaderManager有一个restartLoader()方法可以强制重新加载。不过除非我们需要重新创建Presenter,不然不需要调用这个方法。

通过LoaderCallbacks获取Presenter

LoaderCallbacksActivity/FragmentLoader之间的桥梁。共有三个回调方法:

onCreateLoader():在这里构造Loader实例。

onLoadFinished()Loader在这里传入数据,在这个例子中,也就是Presenter

onLoadReset():在这里清除对于数据的引用。

下面是代码

public class SampleActivity extends AppCompatActivity implements SomeView, LoaderManager.LoaderCallbacks<Presenter> {
 
    private static final int LOADER_ID = 101;
    private Presenter presenter;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        // 初始化代码
        getSupportLoaderManager().initLoader(LOADER_ID, null, this);
    }
 
    @Override
    protected void onStart() {
        super.onStart();
 
        // Presenter可以工作
        presenter.onViewAttached(this);
    }
 
    @Override
    protected void onStop() {
        presenter.onViewDetached();
        super.onStop();
    }
 
    // 其他Activity代码
 
    @Override
    public Loader<Presenter> onCreateLoader(int id, Bundle arg){
        return new PresenterLoader<>(this, new SomeFactoryImpl());
    }
 
    @Override
    public void onLoadFinished(Loader<Presenter> loader, Presenter presenter) {
        this.presenter = presenter;
    }
 
    @Override
    public void onLoaderReset(Loader<Presenter> loader) {
        presenter = null;
    }
}


这段代码的要点有:

Activity/Fragment实例中通过一个唯一的ID初始化Loader

  getSupportLoaderManager().initLoader(LOADER_ID, null, this);

onStart()方法执行时会创建Loader或重新连接到一个已经存在的Loader。在这里我们在onLoadFinished()里创建并传递Presenter对象。由于这些代码都是同步的,所以当onStart()方法执行完后Presenter也可以正常工作了。

如果系统要创建Loader,就会主动去调用onCreateLoader()方法。我们在这个方法中传入正确的PresenterFactory对象并实例化Loader

  @Override

  public Loader<Presenter> onCreateLoader(int id, Bundle arg){

      return new PresenterLoader<>(this, new SomeFactoryImpl());

  }

大功告成!

照例在结尾要总结一下。我们已经看到安卓系统框架中提供的Loader类有如下特点:

在手机状态改变时不会被销毁

会在Activity/Fragment不再被使用后由系统回收。

Activity/Fragment的生命周期绑定,所以事件会自己分发。

每一个Activity/Fragment持有自己的Loader对象的引用,所以可以同时存在多个Presenter-Activity/Fragment组合,比如说在ViewPager中。

可以同步运行,自己确定什么时候数据准备好了可以被传递。

当把这一切组合在一起并进行合理的抽象后,我们就有用来缓存Presenter的工具了,这样Presenter在手机旋转时就不会被销毁了,想活多长或多长。


四、总结

到此,关于 MVP 的简单入门级知识大概就说完了,虽然网上教程很多很多,但还是用自己的话去讲清楚比较舒服。当然了,MVP 可远远不止这些,其他的东西学到之后再提吧。

不过就像开头说的那样,这东西就是一个思想,没必要死板硬套,再者说了谷歌不是又推出了 MVVM了么。说到MVVM 又头疼,感觉总有学不完的东西,虽然总比别人慢一步,但是没办法,学技术得冷静。




作者:iamxiarui
链接:http://www.jianshu.com/p/b507a4dbe3ca
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。





猜你喜欢

转载自blog.csdn.net/chenhande1990chenhan/article/details/78475766