Android:聊聊我所理解的MVP
boblisweer
8年前
<h2><strong>写在前面</strong></h2> <p>最近冷静了一段时间,复习复习之前学的东西。再加上阴阳师一直抽不到SSR,所以打副本的时候想了想毕设项目架构该怎么办。</p> <p>之前看很多开源软件实现都是各种 <strong>MVP</strong> ,看起来很高大上,不过说实话,很早就了解 <strong>MVP</strong> 了,但一直很抗拒去学习,因为觉得模式或者架构类的东西属于一种思想,并不是固定的写法,而学习思想之前,必须要学会在引进这种思想之前是如何处理这些问题的。</p> <p>也就是说,在学 <strong>MVP</strong> 之前,我得弄明白为啥会提出 <strong>MVP</strong> ,因为在 <strong>MVP</strong> 提出之前都是用 <strong>MVC</strong> 去处理的,所以我得学会 <strong>MVC</strong> ,当我能熟练的使用 <strong>MVC</strong> 的时候,再去学习 <strong>MVP</strong> ,这样能很清楚的明白两者之间的区别或者各自的优缺点,个人觉得这样学起来还是比较好的,而不是盲目跟风,包括现在很多博客提到的 <strong>React Native</strong> 、 <strong>Dagger2</strong> 等等都是一样的道理。</p> <p>现在我也来简单聊一聊我自己所理解的 <strong>MVP</strong> ,不过只能算个入门吧。</p> <h2><strong>不可少的介绍</strong></h2> <p>关于 <strong>MVP</strong> 大家或多或少都知道一点,网上关于 <strong>MVP</strong> 的教程也很多,不过优质的就太少了。入门我只看了两篇文章(泡网+鸿洋),文末都会有链接。</p> <p>先上一个经典的图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d720e1b4899edbb5af64f4654b572f03.jpg"></p> <h3><strong>C 和 P 的区别</strong></h3> <p>先来看一下 <strong>MVP</strong> 与 <strong>MVC</strong> 差别在哪?简单一眼扫过,就是 <strong>C</strong> 和 <strong>P</strong> 的差别。</p> <p><strong>1、先看 C</strong></p> <p>C就是 <strong>Controller</strong> ,控制器。负责从 <strong>View</strong> 读取数据,控制用户输入,并向 <strong>Model</strong> 发送数据。简单来说,就是起到一个沟通的作用,能 <strong>很大程度</strong> 上的解决 <strong>Model</strong> 和 <strong>View</strong> 的耦合问题。</p> <p>换句话说就是,它是一个 <strong>Model</strong> 与 <strong>View</strong> 之间的桥梁,让 <strong>Model</strong> 和 <strong>View</strong> 之间不再紧紧关联。</p> <p>比如 <strong>View</strong> 接收到了用户输入数据,先交给 <strong>Controller</strong> , <strong>Controller</strong> 再转交给 <strong>Model</strong> ,反之亦然。</p> <p>这就像小明喜欢隔壁班小红,小明写了一封情书需要通过隔壁班小王,才能交给小红。</p> <p>但是注意,我只是说能很大程度上解决,并不能彻底解决,也就是说小明如果发现了隔壁小王有问题,他仍然可以选择直接把情书交给小红。</p> <p><strong>2、再看 P</strong></p> <p>P就是 <strong>Presenter</strong> ,我翻译成主持者。跟 <strong>C</strong> 类似,仍然是负责 <strong>View</strong> 和 <strong>Model</strong> 之间的沟通。但是它彻底让 <strong>View</strong> 和 <strong>Model</strong> 不能直接沟通。如果想要沟通,就必须通过这个主持者来主持它们两个应该干啥。</p> <p>比如 <strong>View</strong> 接收到了用户输入数据,不能直接给 <strong>Model</strong> ,要交给 <strong>Presenter</strong> , <strong>Presenter</strong> 再转交给 <strong>Model</strong> ,反之亦然。</p> <p>这就像我给主席寄了一个包裹,但这个包裹必须经过重重安检,才能交到主席手上。</p> <p>这就彻底断了我跟主席……哦不对, <strong>Model</strong> 和 <strong>View</strong> 之间的联系。</p> <p><strong>3、简单区别</strong></p> <p>仅从目前来看, <strong>C</strong> 和 <strong>P</strong> 都是为了解放 <strong>Model</strong> 和 <strong>View</strong> 之间的联系,只不过 <strong>C</strong> 是很大程度上解决,但 <strong>P</strong> 是彻底让它们两断了联系。</p> <p>换成技术术语来说就是一句话:</p> <p>C让 <strong>Model</strong> 和 <strong>View</strong> 做到 <strong>松散耦合</strong> ,而 <strong>P</strong> 直接将它们 <strong>解耦</strong> 。</p> <h3><strong>MVC 和 MVP 的区别</strong></h3> <p>知道了各自简单的作用,再来更深层次的理解 <strong>C</strong> 和 <strong>P</strong> 在各自的 <strong>MV+X</strong> 中到底分别做了什么?</p> <p><strong>1、先看 MVC</strong></p> <p>从下图中我们可以看到:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/38a2a5453ad933668c0ce4df65d22134.jpg"></p> <ul> <li> <p>用户 <strong>Event</strong> (事件)会导致 <strong>Controller</strong> 改变 <strong>Model</strong> 或 <strong>View</strong> 或同时改变两者。</p> </li> <li> <p>只要 <strong>Controller</strong> 改变了 <strong>Model</strong> 的数据或属性,所有依赖的 <strong>View</strong> 都会自动更新。</p> </li> <li> <p>类似的,只要 <strong>Controller</strong> 改变了 <strong>View</strong> , <strong>View</strong> 会从潜在的 <strong>Model</strong> 中获取数据进行更新。</p> </li> </ul> <p><strong>2、再看 MVP</strong></p> <p>从下图中我们又能看到:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b18eebf0a02b4580b7189fed10d95cef.jpg"></p> <ul> <li> <p><strong>Presenter</strong> 中同时持有 <strong>View</strong> 以及 <strong>Model</strong> 的 <strong>Interface</strong> 引用,而 <strong>View</strong> 持有 <strong>Presenter</strong> 的实例。</p> </li> <li> <p>当某个 <strong>View</strong> 需要展示某些数据时,首先会调用 <strong>Presenter</strong> 的某个接口,然后 <strong>Presenter</strong> 会调用 <strong>Model</strong> 请求数据。</p> </li> <li> <p>当 <strong>Model</strong> 数据加载成功后会调用 <strong>Presenter</strong> 的回调方法通知 <strong>Presenter</strong> 数据加载完毕,最后 <strong>Presenter</strong> 再调用 <strong>View</strong> 层接口展示加载后数据。</p> </li> </ul> <p><strong>3、主要区别</strong></p> <p>在 <strong>MVC</strong> 中:</p> <ul> <li> <p><strong>View</strong> 可以与 <strong>Model</strong> 直接交互;</p> </li> <li> <p><strong>Controller</strong> 可以被多个 <strong>View</strong> 共享;</p> </li> <li> <p><strong>Controller</strong> 可以决定显示哪个 <strong>View</strong> 。</p> </li> </ul> <p>在 <strong>MVP</strong> 中:</p> <ul> <li> <p><strong>View</strong> 不直接与 <strong>Model</strong> 交互;</p> </li> <li> <p><strong>Presenter</strong> 与 <strong>View</strong> 通过接口来交互,更有利于添加单元测试;</p> </li> <li> <p>通常 <strong>View</strong> 与 <strong>Presenter</strong> 是一对一的,但复杂的 <strong>View</strong> 可能绑定多个 <strong>Presenter</strong> 来处理;</p> </li> <li> <p>Presenter 也可以直接进行 <strong>View</strong> 上的渲染。</p> </li> </ul> <h2><strong>经典案例</strong></h2> <p>当然是那个经典的登录案例,不过这里顺带学下毕设里几个 MD 风格的开源库。先来看一下运行的效果图吧:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ac721c130741b29e40eac1de2846f73a.gif"></p> <h3><strong>先分析</strong></h3> <p>好了,动手之前先分析一下。</p> <p>从上面内容我们知道, <strong>Presenter</strong> 是用来 <strong>Model</strong> 和 <strong>View</strong> 之间交互的。所以必须要持有它们各自的对象,根据需求一般都是用接口来实现。</p> <p>而实现 <strong>View</strong> 层接口的一般都是 <strong>Activity</strong> (暂且这样认为,后文还需要讨论)。</p> <p>当然如果想要 <strong>Activity</strong> 和 <strong>Model</strong> 进行交互,那么这个 <strong>Activity</strong> 中还必须有一个 <strong>Presenter</strong> 的实例,因为需要这个 <strong>Presenter</strong> 来进行交互嘛!</p> <p>OK,把上面所有的东西捋一捋,数一数到底需要啥:</p> <ul> <li> <p><strong>Model</strong> :负责存储、检索、操纵数据,一般都会一些封装对 Bean 进行操作。</p> </li> <li> <p><strong>ModelInterface</strong> :这个不是必须的,但有时候如果几个 Bean 之间有共性,可以抽一个接口出来。</p> </li> <li> <p><strong>View</strong> :暂且就认为是 Activity 。</p> </li> <li> <p><strong>ViewInterface</strong> :View 需要实现的接口,View 和 Presenter 也是通过它来进行交互。</p> </li> <li> <p><strong>Presenter</strong> :最重要的 View 和 Model 的桥梁,处理与用户交互的负责逻辑,需要持有 View 和 Model 的接口对象。</p> </li> </ul> <p>虽然看起来东西确实变多了,但是结构看起来还是很清晰的,扩展起来也比较方便。</p> <h3><strong>再动手</strong></h3> <p>按照上面需要的东西,一步一步来:</p> <p><strong>1、先建一个 Bean</strong></p> <pre> <code class="language-java">/** * @author xiarui 16/09/20 * @description Person的Bean类 */ public class PersonBean { private String name ; private String pwd; //...省略 }</code></pre> <p><strong>2、再建立 Model Interface</strong></p> <p>针对这个 Bean ,有注册和登录的功能,这里强行抽取一个 IPersonModel 接口出来,纯属为了展示用,意义不大:</p> <pre> <code class="language-java">/** * @author xiarui 16/09/20 * @description IPersonModel接口 * @remark 接口其实不必实现 只是为了讲解例子强行抽取的方法 */ public interface IPersonModel { //注册账号 boolean onRegister(String name, String pwd); //登录账号 boolean onLogin(String name, String pwd); }</code></pre> <p><strong>3、其次建立 Model</strong></p> <p>实现了上一步建立的 Model Interface ,主要是对注册和登录方法的实现:</p> <pre> <code class="language-java">** * @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)); } }</code></pre> <p><strong>4、还需要 View Interface</strong></p> <p>在这里我设定了五个方法,其中注册/登录成功与否分别建了两个方法,原因后文再说:</p> <pre> <code class="language-java">/** * @author xiarui 16/09/20 * @description IPersonView接口 */ public interface IPersonView { boolean checkInputInfo(); //检查输入的合法性 void onRegisterSucceed(); //注册成功 void onRegisterFaild(); //注册失败 void onLoginSucceed(); //登录成功 void onLoginFaild(); //登录失败 }</code></pre> <p><strong>5、最重要的 Presenter</strong></p> <p>再次强调, <strong>Presenter</strong> 是用来 <strong>Model</strong> 和 <strong>View</strong> 交互的,而它们各自都实现了接口,那我们只需保证 <strong>Presenter</strong> 持有这些接口即可:</p> <pre> <code class="language-java">/** * @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(); } } }</code></pre> <p><strong>6、最后的 View</strong></p> <p>这里的 <strong>View</strong> 其实就是实现 <strong>IPersonView</strong> 接口的 <strong>Activity</strong> ,它必须有一个 <strong>Presenter</strong> 的实例才能与 <strong>Model</strong> 交互:</p> <p>源码有删减,保留核心方法</p> <pre> <code class="language-java">/** * @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 ==========*/ }</code></pre> <p>当完成这些步骤后,一个简单的 MVP 示例就完成了。</p> <h2><strong>Q & A</strong></h2> <p>这里是一些疑问和解答:</p> <p><strong>Q: MVP 模式中 View 层是否就是 Activity ?</strong></p> <p>A:其实严格意义上来说,这么说是不对的。虽然本例中确实是 <strong>Activity</strong> ,但是在真正的项目中,需要考虑 <strong>Activity</strong> 和 <strong>Fragment</strong> 的情况,甚至还要考虑一些特定的 <strong>View</strong> 或者 <strong>ViewGroup</strong> 。</p> <p>注:后面我就用 Activity 统一指代 View 了。</p> <p><strong>Q:从例子上看,几乎每一个 Activity 都对应着 一个 Presenter ,还需要其他的接口,那如果 Activity 很多怎么办?</strong></p> <p>A:其实这个问题一直是 <strong>MVP</strong> 饱受诟病的地方,虽然 <strong>MVP</strong> 结构很清晰,但确实要增加很多很多的类,所以需要尽量让接口能适用于多种 <strong>View</strong> ,但如果实在忍受不了,建议不用 <strong>MVP</strong> 。</p> <p><strong>Q:使用 MVP 后感觉项目更加臃肿和复杂了怎么办?</strong></p> <p>A:从来都没有人说过 <strong>MVP</strong> 能使得项目简单,只是它会让项目结构更加清晰更加易于扩展而已。就像 <strong>RxJava</strong> 一样,代码量还是那么多,但是流程更加清晰了,这就是能让开发者拥护的原因。</p> <p><strong>Q:为什么案例中 IPersonView 这个接口将注册登录成功与否分开成独立方法?</strong></p> <p>A:这里确实可以不分开,只要将注册/登录的结果作为参数即可,但是这样的话,我们仍然需要在 <strong>Activity</strong> 中根据结果参数来决定显示的 <strong>Toast</strong> 内容。</p> <p>也就是说 <strong>View</strong> 仍然需要处理一些来自 <strong>Model</strong> 的逻辑,这样不是太符合 <strong>MVP</strong> 的意义。所以将判断逻辑放在 <strong>Presenter</strong> 中处理, <strong>View</strong> 层只管展示就行了。</p> <p>包括鸿洋大神的那篇文章中,有一个 <strong>View</strong> 的方法直接传递了涉及 <strong>Model</strong> 层的类,显然违背了 <strong>MVP</strong> 的定义,我觉得不是太好(批判了大神,果断逃……)。</p> <p><strong>Q: Presenter 如果进行耗时操作,但此时对应的 Activity 被杀死,会报空指针么?</strong></p> <p>A:其实在这种情况下,已经存在内存泄漏的情况了。但有意思的是,并不会报空指针,具体原因暂时还不是特别清楚,但好友 <a href="/misc/goto?guid=4959713390277065956" rel="nofollow,noindex">xiasuhuei321</a> 提醒我说,可能回收的时候并没有完全回收,因为系统会认为还存在相关的引用,所以不会空指针。</p> <p><strong>Q:那该如何避免内存泄漏这种情况呢?</strong></p> <p>A:这个问题我看的时候觉得很简单,后来发现这是很有趣的问题。具体方法有很多,也有很多的开源库专门处理这样的问题。其实解决办法归纳起来就是一个 如何让 Presenter 的生命周期跟 Activity 的生命周期保持一致 。</p> <p>我看了很多方法,只觉得通过 <strong>Loader</strong> 的方法来解决是最简单也最有效的方法。但是我还没有彻底学完,暂时不班门弄斧,有兴趣可以直接点击下面的链接进行学习:</p> <h2><strong>总结</strong></h2> <p>到此,关于 <strong>MVP</strong> 的简单入门级知识大概就说完了,虽然网上教程很多很多,但还是用自己的话去讲清楚比较舒服。当然了, <strong>MVP</strong> 可远远不止这些,其他的东西学到之后再提吧。</p> <p>不过就像开头说的那样,这东西就是一个思想,没必要死板硬套,再者说了谷歌不是又推出了 MVVM 了么。说到 MVVM 又头疼,感觉总有学不完的东西,虽然总比别人慢一步,但是没办法,学技术得冷静。</p> <p>当别人大张旗鼓的时候,更要谋自己的路,证自己的道。</p> <h3><strong>参考资料</strong></h3> <p>下面两篇是我的入门教程,写的不错:</p> <p><a href="/misc/goto?guid=4959716630201557355" rel="nofollow,noindex">在Android开发中使用MVP模式 – 泡网</a></p> <p><a href="/misc/goto?guid=4958878904006938877" rel="nofollow,noindex">浅谈 MVP in Android – Hongyang</a></p> <p>下面这个确实对得起标题,真的很详细,主要是一些资源综合,有上下两篇,这里只贴上篇,都很有价值:</p> <p><a href="/misc/goto?guid=4959716630317920976" rel="nofollow,noindex">Android MVP 详解(上)- diygreen</a></p> <p>下面这个是我朋友写的,也很详细而清晰,例子也很具有代表性:</p> <p><a href="/misc/goto?guid=4959716630413265045" rel="nofollow,noindex">Android之MVP初尝试 – xiasuhuei321</a></p> <p>哦对了,这是 MD 风格控件的开源库,扔物线大神的:</p> <p><a href="/misc/goto?guid=4958988896174301949" rel="nofollow,noindex">MaterialEditText – rengwuxian</a></p> <h3> </h3> <p> </p> <p>来自:http://www.iamxiarui.com/2016/09/20/android:聊聊我所理解的mvp/</p> <p> </p>