Android 中的 MVP 与 MVVM

前言

本篇文章主要以通俗易懂的方式去解读 MVPMVVM .帮助大家理解.

如要更深一步的学习与掌握,需要阅读相关源码并自己动手去写.

MVC

要想学习 MVP 与 MVVM ,首先不得不提的是更广为人知的MVC架构.

MVC 架构在网页端使用的更为多一些,比如 php 的 laravel 框架,所用的就是 MVC 架构,而我最一开始就是通过 laravel 接触的 MVC 架构.

先看典型的 MVC 的架构图

mvc.png

View : 即视图层,用户可感官直接察觉到的即可称之为 View 层,比如网站前端显示的页面元素, APP 中的各个应用界面等等.
Model : 即数据操作层,该层进行数据操作和其他一些复杂逻辑操作.
Controller : 即控制层,用户在视图层进行操作时,比如跳转页面时,并不是由 View 层直接对 Model 进行操作,而是通过 Controller 这个中间层去做传递.

20142.jpg

网页端很基础的一个 MVC : 在点击一个链接进行跳转时,首先由路由 Controller 获取该跳转请求,获取以后将根据目标页面去取对应的 Model (页面数据),之后 Model 直接加载至 View上 ,此时页面完成跳转.

看起来并没有什么不对,的确,在网页端这么做确实没什么大问题,因为确实做到了视图层与数据层的分离,代码结构清晰.(没有深入接触过网页端,此处不做深究)...

于是对 Android 也使用 MVC 架构后 :

// HomeActivity.java
public class HomeActivity extends Activity implements View.OnClickListener{
    private TextView textView;
    // 用于数学计算的Model;
    private HomeMathModel model = new HomeMathModel();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        initview()      
        initListener();
    }
    private void initView(){
        textView = findViewById(R.id.text_view);
    }
    private void initListener(){
        textView.setOnClickListener(this);
    }
    @Override
    public void onClick(View view) {
        int id = view.getId();
        if (id == R.id.text_view) {
            textView.setVisibility(model.newNum(Integer.getInteger(textView.getText())) > 0 ?
                View.VISIBLE : View.GONE);
            
        }
    }
}

View 省略,仅仅显示一个 textView .
Model 省略, model.newNum(int num) 这里对数值进行随机加减10以内的数.

可以看出, HomeActivity 这里起到了 Controller 的作用,将点击事件的逻辑操作交由 HomeMathModel ,并根据结果对 View 进行修改.

但是 Controller 持有了页面的控件对象,同时也持有了 Model 的对象.而 View 并不能自主进行页面变化,需要 Controller 辅助操作.

你可能会说,可以让 Model 直接返回 VISIBLE 或者 GONE 啊.那么这样子 Model 将会与 View 的耦合性更高,当另一个页面想要根据计算结果设置文字大小,那么将完全无法重用该 Model .

MVC 总结

Android 中的 MVC 有着以下致命缺点 :

  1. View 层与 Model 层为相互感知的,即 Model (数据操作)仅仅针对该页面,复用性查 - 耦合性高
  2. View 层的作用相对较小,无法进行复杂操作. - 功能性弱
  3. Model 层与 Controller 层写在一起 - 结构混乱
  4. 当页面过为复杂时, Acitvity 中很容易出现数千行代码 - 难以维护

基于此,衍生出了更适合 Android 的 MVP 架构和 MVVM 架构.

MVP

mvp.png

MVP 与 MVC 最大的区别是变了个字母(废话),将原本的 Controller 改变为了 Presenter .

如图所示,改变后的 MVP 架构,其操作请求会发送至 Presenter ,由 Presenter 去操作数据,并且将 Model 返回的数据做进一步的处理返回给 View 层进行展示.

单从图中可以看出与 MVC 架构的不同 : Model 层与 View 层不再是互相感知的了.

View 不会想知道是谁处理我的数据,我只需要拿到结果后进行展示即可.
Model 不会想知道我处理的是谁的数据,我只需要根据传入值进行处理后返回结果就好.

此时 Presenter 就相当于一个纽带的作用,去协调 View 与 Model的连接.下面将 MVC 转变为 MVP :

20142.jpg

// HomeActivity.java
public class HomeActivity extends Activity implements View.OnClickListener, MathInterface{
    private TextView textView;
    private MathPresenter presenter = new MathPresenter();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        initview()      
        initListener();
    }
    private void initView(){
        textView = findViewById(R.id.text_view);
    }
    private void initListener(){
        textView.setOnClickListener(this);
    }
    @Override
    public void onClick(View view) {
        int id = view.getId();
        if (id == R.id.text_view) {
            prensenter.newRandom(Integer.getInteger(textView.getText()), this);          
        }
    }
    @Override
    public void onMathRandom(int newNum) {
         textView.setVisibility(newNum > 0 ? View.VISIBLE : View.GONE);
    }
}

首先对 HomeActivity 的改造,去除对 Model 的持有,大家可以发现这里新引入了 MathPresenter ,与 MVC不同,MVC 中的 View 层对应了 xml 的布局文件,考虑到 xml 的功能性过小,在 MVP 中将由 Activity 与 xml 布局文件共同承担着 View 层的功能.而 Presenter 层的功能 Activity 不再参与,交由 MathPresenter 实现,View 层仅需要调用即可.

View 层与 Presenter 的交互模式是通过接口回调进行交互,在调用 Presenter 的方法时,传入实现了的接口方法,在 Presenter 操作完毕后通过接口回调将结果返回给 View 层.

// MathInterface.java
public interface MathInterface{
    /**
     * 随机数操作结果回调
     * @param newNum 做随机数操作后的新值
     */
    void onMathRandom(int newNum);
}

接口很简单,只有一个方法,当随机数操作完成后回调该方法传值,下面看一下 Presenter 的代码.

// MathPresenter.java
public class MathPresenter {
    public void newRandom(int num, MathInterface interface){
        HomeMathModel model = new HomeMathModel();
        interface.onMathRandom(model.newNum(num));
    }
}

从 MathPresenter 可以看出,其充当着 View 与 Model 的桥梁,接口是其通信的介质,Presenter 也不需要知道谁在调用自己,只需要你实现了接口,那么是谁就无所谓了.此特性就保证了 Presenter 的复用性,与 View 的耦合性降低.

当 MVC 中多个 Activity 持有了同一个 Model 时,如果更换修改持有的 Model 时,修改量较大.而在 MVP 中,Model 仅被 Presenter感知.只需要 Presenter 中的 Model 替换之后,将一次性应用到所有.

MVP 总结

优势 :

  1. Model 与 View 不再互相感知. - 低耦合
  2. Presenter 的方法在相同场景多次使用 - 高复用性
  3. 交互主要发生在 Presenter 内部,由 Presenter 持有 Model - 易于扩展
  4. 由于交互逻辑写在了 Presenter 内, 功能模块相对独立. - 便于单元测试

劣势 :

由于 View 与 Presenter 是通过接口进行通信,会出现每增加一个新功能都会需要编写一个新的接口,导致项目中会有大量的接口文件存在,并且会由于项目的复杂度同步增加.

MVVM

mvvm.png

可以看出,和 MVP 的架构又发生了一些变化,不仅仅是 Presenter 更换为了 ViewModle (废话).

这里的 ViewModel 与 View 进行了绑定,绑定的结果就是, View 的页面变化会直接影响到 ViewModel 的数据,而 ViewModel 的数据变化同时也会导致 View 界面的变化.在 MVVM 的架构中,我们再绑定完成后仅仅需要考虑的只有对数据的处理(Model),简化了对页面进行操作的逻辑,同时也放大了 View 层的作用,类比来说可以说是一个静态的网页转变成了动态网页.

此处拿谷歌爸爸的 Databingding 框架举例. 如若想要使用 Databingding ,只需要在 build.gradle 加入如下代码即可 :

// build.gradle
...
android {
    ...
    dataBinding {
        enabled = true
    }
    ...
}
...

举个例子

// activity_home.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:bind="http://schemas.android.com/tools">
    <data>
        <variable
            name="activity"
            type="com.bt.helper.HomeActivity"/>
        <variable
            name="data"
            type="com.bt.helper.MathViewModel"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView>
            android:id="@+id/textview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@{data.num}"
            android:onClick="@{activit::onClick}"/>      
    </LinearLayout>
</layout>

MVVM 架构中的 xml 已经可以承担绝大部分的 View 的功能需求.我们可以看出 xml 布局与寻常的 xml 代码有些不同:

  1. 最外层的 ViewGroup 必须为 layout 标签,其专用于 DataBinding 框架,并且可以自动生成 Binding 类,该类后续会用到.
  2. data 标签内需要声明所需数据及其类型.
  3. 设置 TextView 的文本和点击事件可以直接使用数据源进行设置.

接下来看下 ViewModel 的写法

// MathViewModel.java
public class MathViewModel extends BaseObservable{

    private int num;

    public MathViewModel () {
    }

    @Bindable
    public String getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num= num;
        notifyPropertyChanged(BR.num);
    }   
}

ViewModel 和我们常见的模板类很相似,包含了成员变量和其 getter 和 setter方法.不同的是 :

  1. 其需要继承 BaseObservable 类.
  2. @Bindable注解可以让我们在 xml 直接用获取 public 成员变量的形式去获取该私有变量.
  3. setter 方法中多了 notifyPropertyChanged(BR.num),他可以在我们调用 setter 方法时去更新视图,否则不会造成页面的上数据的变化.

设置完 ViewModel 和 View 之后,剩下的事情就是进行数据处理

public class HomeActivity extends Activity implements View.OnClickListener{
    private MathViewModel viewModel = new MathViewModel();
    private ActivityHomeBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 设置绑定视图
        binding = DataBindingUtil.setContentView(this, R.layout.activity_home);
        // 设置数据绑定
        binding.setActivity(this);
        binding.setData(viewModel);
    }
    @Override
    public void onClick(View view) {
        int id = view.getId();
        if (id == R.id.text_view) {
            viewModel.setNum(new Model().newNum(viewModel.getNum());
        }
    }
}

根据代码可以看到,代码精简了不止一点,不用去繁琐的获取 View 然后对其操作,不用再手动对其设置监听.

分析一下代码,我们做了如下事情 :

  1. 声明了一个 ViewModel ,此 ViewModel 将作为数据源与 xml 布局文件进行绑定
  2. 声明了一个 ActivityHomeBinding ,此类是根据 xml 文件自动生成,我们的布局文件为activity_home.xml,自动生成将会把他转为大驼峰式命名 + Binding.
  3. 设置数据源,此处绑定了2个数据源.
  4. 处理点击事件,由于我们将activity当做数据源传入,点击时会自动查找 onClick 方法进行调用, 当 ViewModel 中的元素调用了 setter 方法时将会同步更新 xml 中的显示.

最基本的基于 MVVM 的 Databinding 框架已经完成.MVVM 的框架更倾向于 View 的强功能性,使得 View 的动态改变不再依赖于大量的代码,在数据源改变时就可以做到动态改变,使得我们可以专心于处理数据.(虽然看起来代码量不多,实际在我们编写 xml 布局文件时,谷歌爸爸已经帮助我们生成了大量的辅助代码,只是这些是我们无感知的.总之写起来很爽就行了不是么)

总结

优势 :

  1. View 与 ViewModel 存在对应关系,虽然两者的耦合性较高,但是由于视图具备重用性,使得 ViewModel 也具备很高的重用性(说耦合性高可能不合适,因为 ViewModel 实际上就是针对对应的 View 服务的,而且其代码更近似于模板而非逻辑代码)
  2. 在设置属性的过程中不必担心空指针问题(这应该算是 DataBinding 的优势吧...)
  3. 避免编写大量接口,省时省力.(相对于 MVP )
  4. 结构清晰.
  5. 代码量显著减少,便于维护

劣势 :

  1. 由于binding是由编译器自动生成,可能会不知不觉中 APK 包会相对其他架构大很多
  2. 在 xml 中的代码不容易被检查,导致如果出现 BUG 排查起来相对困难

以上为笔者个人对 MVP 及 MVVM 的浅略介绍,如有错误之处欢迎指正,希望能够帮助初次接触 MVP 与 MVVM 的各位.


Last modification:November 15th, 2018 at 02:35 pm
如果觉得我的文章对你有用,请我喝杯咖啡

Leave a Comment

2 comments

  1. NBB

    NBB

  2. lovepf

    可以的,mvvm清晰易懂