Android MVP 模式解析与基本实现方式
前言
记得自己接手的第二个项目采用的是 MVP 模式进行开发的,当时架构已经设计好,我看了几篇关于 MVP 的文章,对其有了基本的了解之后,便照猫画虎进行了开发,之后便再也没接触过 MVP。
最近空闲的时候读了一篇 MVP 相关的文章,受益匪浅。于是打算写一篇关于它的文章,一方面是作为自己的学习笔记方便查看,另一反面希望能给没有接触过 MVP 模式的新人提供帮助,以便可以快速入门。
什么是 MVC
在讲 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
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 接口(接口或类名自己定义,一般有约定成俗的写法),把所有业务逻辑的接口都放在这里,并创建它的实现类 PresenterImpl。
- 创建 IView 接口,把所有视图逻辑的接口都放在这里,其实现类是Activity/Fragment。
- 在 Activity/Fragment 中包含了一个 IPresenter 的实例,而 PresenterImpl 里又包含了一个 IView 的实例并且依赖了 Model。Activity/Fragment 只保留对 IPresenter 的调用,当 View 层发生某些请求响应或者生命周期发生变化,则会迅速的向 Presenter 层发起请求,让 Presenter 做出相应的处理。
- Model 并不是必须有的,但是一定会有 View 和 Presenter。
我们还是以上面的功能为例,用 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 的优势与不足。
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
上一篇: junit的简单使用