美文网首页
Android:聊聊我所理解的MVP

Android:聊聊我所理解的MVP

作者: iamxiarui | 来源:发表于2018-07-02 20:48 被阅读0次

    写在前面

    最近冷静了一段时间,复习复习之前学的东西。再加上阴阳师一直抽不到SSR,所以打副本的时候想了想毕设项目架构该怎么办。

    之前看很多开源软件实现都是各种 MVP ,看起来很高大上,不过说实话,很早就了解 MVP 了,但一直很抗拒去学习,因为觉得模式或者架构类的东西属于一种思想,并不是固定的写法,而学习思想之前,必须要学会在引进这种思想之前是如何处理这些问题的。

    也就是说,在学 MVP 之前,我得弄明白为啥会提出 MVP ,因为在 MVP 提出之前都是用 MVC 去处理的,所以我得学会 MVC ,当我能熟练的使用 MVC 的时候,再去学习 MVP ,这样能很清楚的明白两者之间的区别或者各自的优缺点,个人觉得这样学起来还是比较好的,而不是盲目跟风,包括现在很多博客提到的 React NativeDagger2 等等都是一样的道理。

    现在我也来简单聊一聊我自己所理解的 MVP ,不过只能算个入门吧。

    不可少的介绍

    关于 MVP 大家或多或少都知道一点,网上关于 MVP 的教程也很多,不过优质的就太少了。入门我只看了两篇文章(泡网+鸿洋),文末都会有链接。

    先上一个经典的图:

    MVC和MVP

    C 和 P 的区别

    先来看一下 MVPMVC 差别在哪?简单一眼扫过,就是 CP 的差别。

    1、先看 C

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

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

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

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

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

    2、再看 P

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

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

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

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

    3、简单区别

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

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

    CModelView 做到 松散耦合,而 P 直接将它们 解耦

    MVC 和 MVP 的区别

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

    1、先看 MVC

    从下图中我们可以看到:

    MVC
    • 用户 Event(事件)会导致 Controller 改变 ModelView 或同时改变两者。
    • 只要 Controller 改变了 Model 的数据或属性,所有依赖的 View 都会自动更新。
    • 类似的,只要 Controller 改变了 ViewView 会从潜在的 Model 中获取数据进行更新。

    2、再看 MVP

    从下图中我们又能看到:

    MVP
    • Presenter 中同时持有 View 以及 ModelInterface 引用,而 View 持有 Presenter 的实例。
    • 当某个 View 需要展示某些数据时,首先会调用 Presenter 的某个接口,然后 Presenter 会调用 Model 请求数据。
    • Model 数据加载成功后会调用 Presenter 的回调方法通知 Presenter 数据加载完毕,最后 Presenter 再调用 View 层接口展示加载后数据。

    3、主要区别

    MVC 中:

    • View 可以与 Model 直接交互;
    • Controller 可以被多个 View 共享;
    • Controller 可以决定显示哪个 View

    MVP 中:

    • View 不直接与 Model 交互;
    • PresenterView 通过接口来交互,更有利于添加单元测试;
    • 通常 ViewPresenter 是一对一的,但复杂的 View 可能绑定多个 Presenter 来处理;
    • Presenter 也可以直接进行 View 上的渲染。

    经典案例

    当然是那个经典的登录案例,不过这里顺带学下毕设里几个 MD 风格的开源库。先来看一下运行的效果图吧:

    项目演示

    先分析

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

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

    而实现 View 层接口的一般都是 Activity (暂且这样认为,后文还需要讨论)。

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

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

    • Model:负责存储、检索、操纵数据,一般都会一些封装对 Bean 进行操作。
    • ModelInterface:这个不是必须的,但有时候如果几个 Bean 之间有共性,可以抽一个接口出来。
    • View:暂且就认为是 Activity 。
    • ViewInterface:View 需要实现的接口,View 和 Presenter 也是通过它来进行交互。
    • Presenter:最重要的 View 和 Model 的桥梁,处理与用户交互的负责逻辑,需要持有 View 和 Model 的接口对象。

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

    再动手

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

    1、先建一个 Bean

    /**
     * @author xiarui 16/09/20
     * @description Person的Bean类
     */
    public class PersonBean {
        private String name ;
        private String pwd;
        //...省略
    }
    

    2、再建立 Model Interface

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

    /**
     * @author xiarui 16/09/20
     * @description IPersonModel接口
     * @remark 接口其实不必实现 只是为了讲解例子强行抽取的方法
     */
    public interface IPersonModel {
        //注册账号
        boolean onRegister(String name, String pwd);
        //登录账号
        boolean onLogin(String name, String pwd);
    }
    

    3、其次建立 Model

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

    **
     * @author xiarui 16/09/20
     * @description Model类 实现IPersonModel接口
     * @remark 接口其实不必实现 只是为了讲解例子强行实现的
     */
    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

    在这里我设定了五个方法,其中注册/登录成功与否分别建了两个方法,原因后文再说:

    /**
     * @author xiarui 16/09/20
     * @description IPersonView接口
     */
    public interface IPersonView {
        boolean checkInputInfo();  //检查输入的合法性
        void onRegisterSucceed();  //注册成功 
        void onRegisterFaild();    //注册失败 
        void onLoginSucceed();     //登录成功
        void onLoginFaild();       //登录失败
    }
    

    5、最重要的 Presenter

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

    /**
     * @author xiarui 16/09/20
     * @description Person的Presenter类
     * @remark 必须要传M和V 因为P需要控制M和V
     */
    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 交互:

    源码有删减,保留核心方法

    /**
     * @author xiarui 16/09/20
     * @description MVP的简单例子
     * @remark View 必须持有 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() {
            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 ==========*/
    }
    

    当完成这些步骤后,一个简单的 MVP 示例就完成了。

    Q & A

    这里是一些疑问和解答:

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

    A: 其实严格意义上来说,这么说是不对的。虽然本例中确实是 Activity ,但是在真正的项目中,需要考虑 ActivityFragment 的情况,甚至还要考虑一些特定的 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: 其实在这种情况下,已经存在内存泄漏的情况了。但有意思的是,并不会报空指针,具体原因暂时还不是特别清楚,但好友xiasuhuei321提醒我说,可能回收的时候并没有完全回收,因为系统会认为还存在相关的引用,所以不会空指针。


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

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

    我看了很多方法,只觉得通过 Loader 的方法来解决是最简单也最有效的方法。但是我还没有彻底学完,暂时不班门弄斧,有兴趣可以直接点击下面的链接进行学习:

    通过Loader延长Presenter生命周期

    总结

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

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

    当别人大张旗鼓的时候,更要谋自己的路,证自己的道。

    参考资料

    下面两篇是我的入门教程,写的不错:

    在Android开发中使用MVP模式 - 泡网

    浅谈 MVP in Android - Hongyang

    下面这个确实对得起标题,真的很详细,主要是一些资源综合,有上下两篇,这里只贴上篇,都很有价值:

    Android MVP 详解(上)- diygreen

    下面这个是我朋友写的,也很详细而清晰,例子也很具有代表性:

    Android之MVP初尝试 - xiasuhuei321

    哦对了,这是 MD 风格控件的开源库,扔物线大神的:

    MaterialEditText - rengwuxian

    项目源码

    FirstMVPDemo - IamXiaRui


    个人博客:www.iamxiarui.com
    原文链接:http://www.iamxiarui.com/?p=890

    相关文章

      网友评论

          本文标题:Android:聊聊我所理解的MVP

          本文链接:https://www.haomeiwen.com/subject/ckcruftx.html