文章同步自简书:https://www.jianshu.com/p/48504ee1d157
记得自己接手的第二个项目采用的是 MVP 模式进行开发的,当时架构已经设计好,我看了几篇关于 MVP 的文章,对其有了基本的了解之后,便照猫画虎进行了开发,之后便再也没接触过 MVP。
最近空闲的时候读了一篇 MVP 相关的文章,受益匪浅。于是打算写一篇关于它的文章,一方面是作为自己的学习笔记方便查看,另一反面希望能给没有接触过 MVP 模式的新人提供帮助,以便可以快速入门。
在讲 MVP 之前,我们先来了解一下 MVC。
MVC 模式是经典的三层架构一种具体的实现方式,全称为 Model(模型层) 、View(视图层)、Controller(控制器)。下面介绍一下它们各自的职责:
Model 层:用来定义实体对象,处理业务逻辑,可以简单地理解成 Java 中的实体类。View 层:负责处理界面的显示,在 Android 中对应的就是 xml 文件。Controller 层:对应的是 Activity/Fragment ,当加载完成 xml 布局之后,我们需要找到并设置布局中的各个 View,处理用户的交互事件,更新 View 等。下面我们通过一个简单的例子来说明这三者是如何交互的。
首先是 View 层,布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <EditText android:id="@+id/et_height" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="身高cm"/> <EditText android:id="@+id/et_weight" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="体重kg"/> <Button android:id="@+id/btn_cal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal"/> </LinearLayout>然后是 Controller 层:
public class MVCActivity extends AppCompatActivity implements View.OnClickListener { private EditText mEtHeight; private EditText mEtWeight; private Button mBtnCal; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Controller 访问了 View 的组件 mEtHeight = findViewById(R.id.et_height); mEtWeight = findViewById(R.id.et_weight); mBtnCal = findViewById(R.id.btn_cal); // 这个点击事件属于 View,它是 View 的监听器 mBtnCal.setOnClickListener(this); // Controller 调用了 Model String btnText = User.instance().getBtnText(); // 然后 Controller 更新了 View 的属性 mBtnCal.setText(btnText); } @Override public void onClick(View v) { int height = Integer.parseInt(mEtHeight.getText().toString()); float weight = Float.parseFloat(mEtWeight.getText().toString()); // Controller 更新了 Model 中的数据 User.instance().setHeight(height); User.instance().setWeight(weight); // 这里 View 又访问了 Model 的数据,并呈现在 UI 上 String valueBMI = String.valueOf(User.instance().getBMI()); Toast.makeText(this, "BMI: " + valueBMI, Toast.LENGTH_LONG).show(); } }最后是 Model 层:
public class User { private int height; private float weight; private static User mUser; public static User instance(){ if (mUser == null) { synchronized (User.class) { if (mUser == null) { mUser = new User(); } } } return mUser; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public float getWeight() { return weight; } public void setWeight(float weight) { this.weight = weight; } public String getBtnText() { // 在这里,我们可以从数据库中查询数据 // 或者访问网络获取数据 return "计算BMI"; } public float getBMI() { // 通过已有的属性计算出新的属性,也属于业务逻辑的操作 return weight / (height * height) * 10000; } }从上面的代码中,我们可以看到 View 层的职责是非常简单的,向用户呈现 xml 文件中的布局,并且响应用户的触摸事件。
而 Controller 层的职责逻辑则复杂很多。它对于 View 层,需要将从 Model 中获取到的数据及时地呈现在 UI 上。而对于 Model 层,当 app 的生命周期发生变化或者接收到某些响应时,需要对 Model 的数据进行 CRUD。在这个例子中,用户点击按钮的时候,首先获取 View 层用户的输入,然后更新 Model 层的属性,最后获取到 Model 层计算得出的新数据并显示在 UI 上。
对于 Model 来说,它不仅仅是个简单的实体类,还应该包括数据处理与业务逻辑的操作,比如说对数据库的操作、网络请求等,但是很多情况下,我们很少把这些操作写在实体类中。
demo 运行效果如下:
在 MVC 模式中,Controller 层扮演着重要的角色,它不仅要处理 UI 的显示与事件的响应,还要负责与 Model 层的通信,同时 Model 层与 View 层也会通信,三者的耦合度很大。
作为 Android 开发中默认使用的架构模式,MVC 易于上手,适合快速开发一些小型项目。但是随着业务逻辑的复杂度越来越大,Activity/Fragment 会越来越臃肿,因为它同时承担着 Controller 与 View 的角色,这对于项目后期的更新维护与测试交接都是非常不方便的,大大提高了生产成本。这么一来,它就违背了 “提高生产力” 的初衷,于是 MVP 模式就应运而生了。
MVP 是 MVC 的一种升级进化,全称为 Model(模型层)、View(视图层)、Presenter(主持者)。从结构图中,我们可以看到它与 MVC 的区别:Presenter 代替了 Controller,去除了 View 与 Model 的关联与耦合。
Model 层:和 MVC 模式中的 Model 层是一样的,这里不再说了。View 层:视图层。在 MVP 中,它不仅仅对应 xml 布局了,Activity/Fragment 也属于视图层。View 层现在不仅作为 UI 的显示,还负责响应生命周期的变化。Presenter 层:主持者层,是 Model 层与 View 层进行沟通的桥梁,处理业务逻辑。它响应 View 层的请求从 Model 层中获取数据,然后将数据返回给 View 层。在 MVP 的架构中,最大的特点就是 View 与 Model 之间的解耦,两者之间必须通过 Presenter 来进行通信,使得视图和数据之间的关系变得完全分离。但是 View 和 Presenter 两者之间的通信并不是想怎么调用就可以怎么调用的,下面讲一下 MVP 模式最基本的实现方式。
我们还是以上面的功能为例,用 MVP 模式具体实现它。
IPresenter 接口:
public interface IPresenter { /** * 调用该方法表示 Presenter 被激活了 */ void start(); void onBtnClick(int height, float weight); /** * 调用该方法表示 Presenter 要结束了 * 为了避免相互持有引用而导致的内存泄露 */ void destroy(); }IView 接口:
public interface IView { /** * 用来更改按钮的文本 * * @param text */ void updateBtnText(String text); /** * 用来弹出吐司显示 BMI * * @param bmi */ void showToast(float bmi); }IPresenter 接口的实现类 PresenterImpl:
public class PresenterImpl implements IPresenter { private IView mView; public PresenterImpl(IView mView) { this.mView = mView; } @Override public void start() { String text = User.instance().getBtnText(); mView.updateBtnText(text); } @Override public void onBtnClick(int height, float weight) { User.instance().setHeight(height); User.instance().setWeight(weight); float bmi = User.instance().getBMI(); mView.showToast(bmi); } @Override public void destroy() { mView = null; } }IView 接口的实现类 MVPActivity:
public class MVPActivity extends AppCompatActivity implements IView, View.OnClickListener { private EditText mEtHeight; private EditText mEtWeight; private Button mBtnCal; private IPresenter mPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 实例化 PresenterImpl mPresenter = new PresenterImpl(this); // View 的相关初始化 mEtHeight = findViewById(R.id.et_height); mEtWeight = findViewById(R.id.et_weight); mBtnCal = findViewById(R.id.btn_cal); mBtnCal.setOnClickListener(this); } @Override protected void onStart() { super.onStart(); mPresenter.start(); } @Override public void onClick(View v) { int height = Integer.parseInt(mEtHeight.getText().toString()); float weight = Float.parseFloat(mEtWeight.getText().toString()); mPresenter.onBtnClick(height, weight); } @Override public void updateBtnText(String text) { mBtnCal.setText(text); } @Override public void showToast(float bmi) { Toast.makeText(this, "BMI: " + bmi, Toast.LENGTH_LONG).show(); } @Override protected void onDestroy() { if (mPresenter != null) { mPresenter.destroy(); mPresenter = null; } super.onDestroy(); } }Model 层的代码与 MVC 例子中的相同,这里就不再帖代码了。
看完代码可能有的人会发现,相对于 MVC 模式来说,代码不仅没有减少,反而还增加了许多接口,看起来有些晕。但我们仔细观察可以发现,虽然增加了许多接口,但是 MVP 的结构是非常清晰的,也是有很大的好处的,下面我们仔细分析一下。
MVPActivity 实现了IView 接口,并实现了 updateBtnText(..) 和showToast(..) 这两个方法,但是这两个方法看起来好像都没有被调用,只是在 onCreate() 的时候创建了一个 PresenterImpl 对象,在 onStart() 的时候调用了 mPresenter.start() 方法,然后在 onDestroy() 的时候调用了 mPresenter.destroy() 方法,而当按钮的点击事件响应的时候又调用了 mPresenter.onBtnClick(..) 方法,那么既没有回调也没有直接调用,那 IView 中的两个接口方法又是何时何地被调用的呢?接下来我们将继续分析 Presenter 层的实现代码。
在 PresenterImpl 中实现了 IPresenter 接口并实现了 start() onBtnClick(..) destroy() 方法,在构造方法中有一个IView的参数,这个对象是 IView 的引用,这个对象可以是 Activity 或者是 Fragment 也可以是 IView 接口的任何一个实现类,但对于 PresenterImpl 而言具体的 IView 到底是谁并不知道。在 PresenterImpl 中,在 start() 和 onBtnClick() 方法中除了调用 Model 外都调用了 IView 的方法:mView.updateBtnText(..) 和 mView.showToast(..),以此来对 View 层的 UI 呈现以及交互提醒做出相应的响应。而最后的 destroy() 方法则是用于释放对 IView 的引用。
由此我们可以得出几个结论:
对于 View 而言:
我需要一位主持者,当出现视图相关事件的响应或者生命周期的变化时,我需要告诉这位主持者,我想要做些什么。我会提供一系列通用接口,以便于当主持者完成我的请求后,调用相应的接口告诉我这件事的结果。我所有的请求都发给主持者,让他帮我做决定,但是这件事是怎么做的,我并不知道也不关心,我只是需要结果。对于 Presenter 而言:
我接收到 View 的请求后找 Model 寻求帮助,等 Model 做完事情后通知我了,我在把结果告诉 View。我只知道指挥 Model做事、告诉 View 显示数据,但我不干活。我相当于一座桥,连接着 View 和 Model,他们谁也不认识谁,想要通信必须要通过我,如果没有我,他们两永远都不会认识。没错,我就是这么重要。由于有 Presenter 的存在,View 层的代码看起来是非常清晰的,每一个方法都有它自己的功能职责,彼此之间并不会相互耦合。而 Presenter 中的代码也是如此,每一个方法都只处理一件事,并不会做其他无相关的事情。另外我们观察到,在 MVPActivity 中并没有直接对 PresenterImpl 进行持有,而是持有了一个 IPresenter 对象;同样的在 PresenterImpl 也并没有直接持有 MVPActivity 而是持有了一个 IView 对象。也就是说,凡是实现了 IPresenter 便是 Presenter 层,凡是实现了 IView 便是 View 层,这样就能很方便地变更业务逻辑或者进行单元测试。下面就讲一讲 MVP 的优势与不足。
优势:
解耦,抽这么多接口出来就是为了解耦,非常适合多人协同开发。各模块分工明确,结构清晰。在 MVC 模式中,Activity/Fragment 兼顾着 Controller 与 View 的作用,杂乱且难以维护,而 MVP 模式大大减少了 Activity/Fragment 的代码,容易看懂、容易维护和修改。方便地变更业务逻辑。比如有三个功能,它们的 View 层完全一致,只是各自的业务逻辑不同,那么我们可以分别创建三个不同的 PresenterImpl (当然他们都要实现 IPresenter 接口),然后在 Activity 中创建 IPresenter 对象的时候,就可以根据不同的外部条件创建出不同的 PresenterImpl,这样就能方便的实现它们各自的业务。方便进行单元测试。由于业务逻辑都是在 IPresenter 中实现的,那么我们可以创建一个 PresenterTest 实现 IPresenter 接口,然后把 MVPActivity 中对 PresenterImpl 的创建改成 PresenterTest 的创建,然后就可以对 IView 的方法随意进行测试了。如果想要测试 IPresenter 中的方法,那就新建一个 ViewTest 类实现 IView 接口,然后将其传入 PresenterImpl,便可以自由的测试 IPresenter 中的方法是否有效。避免 Activity 内存泄露。Activity 是有生命周期的,用户随时可能切换 Activity,当 APP 的内存不够用的时候,系统会回收处于后台的 Activity 的资源以避免 OOM。采用传统的模式,一大堆异步任务都有可能保留着对 Activity 的引用,比如说许多图片加载框架。这样一来,即使 Activity 的 onDestroy() 已经执行,这些 异步任务仍然保留着对 Activity 实例的引用, 所以系统就无法回收这个 Activity 实例了,结果就是 Activity Leak。Android 的组件中,Activity 对象往往是在堆里占最多内存的,所以系统会优先回收 Activity 对象, 如果有 Activity Leak,APP很容易因为内存不够而 OOM。采用 MVP 模式,只要在当前的 Activity 的 onDestroy() 里,分离异步任务对 Activity 的引用,就能避免 Activity Leak。不足:
有点笨重,不适合短期小型的项目开发。你一个 Activity 就能搞定的事,非要用 MVP 干嘛。虽然 Activity 变得轻松了,但是 Presenter 的业务越来越复杂。提高了学习成本,由于 MVP 的变种非常多,需要自己在实战中慢慢摸索。1.关于 MVP 的分包结构,有的人习惯按照下面这种方式分包:
将所有的 Model/View/Presenter 的代码分别放在同一个包下,这样业务多了会很乱。也有人喜欢按照模块分包,将同一个功能模块的 Model/View/Presenter 放在一个模块包下。具体的分包方式还是要按照具体的项目和自己的喜好来定。
2.在使用上述 MVP 模式进行开发的过程中,还遇到了空指针的问题。当 Presenter 中通过异步方式获取数据然后需要更新 View 的时候,这个时候 View 有可能已经消失了,极度容易引起 NullPointerException。比如下面的示例代码:
@Override public void login(String phone, String pwd) { OkGo.<BaseModal<User>>get(url).tag(this) .params(AppInterface.getLoginParams(phone, pwd)) .execute(new JsonCallback<BaseModal<User>>() { @Override public void onSuccess(Response<BaseModal<User>> response) { if (mView == null) { return; } mView.showToast("登录成功"); } @Override public void onError(Response<BaseModal<User>> response) { if (mView == null) { return; } mView.showToast("登录失败"); } }); }由上面的代码可以看出,在 Presenter 进行异步回调后,一定要对 mView 进行非空判断,否则会出现大面积的 NullPointerException。
以上就是 MVP 模式基本的实现方式,可能示例代码太简单无法体现 MVP 的优势,但是真正地理解了它并在项目中实际使用,你便能体会到它所带来的好处。MVP 有很多变种与改进,网上也有很多资料,如果想学的话,可以很方便地找到。另外,Google 官方也开源了一系列 Andorid 架构的使用示例,其中就包括了 MVP 模式,地址:https://github.com/googlesamples/android-architecture 。
本篇博客示例代码:https://github.com/ayuhani/mvp_demo
欢迎关注我的微信公众号