1、MVVM简介
MVVM是相对于MVC和MVP的一个概念,是一种架构模式。
1.1 MVC
传统的MVC中,View改变通知Controller进行处理,Controller处理结束后通知Model层更新,Model层更新以后通知View层渲染,指令单项流动,角色分工明确。但是MVC有三个缺点,1、三个角色互相持有对方依赖,因此很难复用其中任意一方;2、开发时必须三个模块同步开发,否则很难相互匹配;3、由于每一个角色的改变都会直接或间接的影响另外两个角色,所以任何改动都必须考虑全盘影响。
1.2 MVP
MVP解决了以上三个问题,MVP中的Presenter层相当于MVC中的Controller层,但有一个变动:Presenter分别和Model层以及View层双向交互,而Model层与View层之间不再直接交互,并且Presenter来定义Model层和View层各自要实现的功能。这个变动解决了之前所提的三个问题,首先Model层和View层可以任意替换,只要代替者能实现Presenter层定义的接口即可,这样保证了Model层和View层的可复用性;其次在Presenter层定义了Model层和View层需要实现的功能后,Model层和View层可以分别开发,有Presenter层去处理两者适配的问题;最后Model层和View层脱离以后任意一方的改动只要Presenter层进行适配即可,不会再影响到对方,降低了修改代码时的影响范围。
1.3 MVVM
MVVM是MVP模式进一步发展的产物,通过语言或框架的支持,开发人员不需要再手动处理Model改变以后View的更新,而是通过订阅-观察的模式让View在Model改变时自动更新。这个模式虽然有一定的学习成本,但优点(尤其是在Android端)也清晰可见:1、继承了MVP的所有优点,拥有较好的可复用性和可维护性,并且view层和model层可以分别开发;2、方便测试,由于Model和View在框架层面上进行绑定,理论上只要数据正常且绑定的方式合理,View显示就不会有问题,这样可以针对Model层和ViewModel层进行单元测试,而不用再考虑View层;3、Android中的生命周期、activity重建等问题不需要开发人员考虑,由jetpack提供的组件进行处理。
Android端的MVVM架构的实现基于jetpack组件包中的四个组件:Databinding、LiveData、ViewModel、Lifecycle,接下来我们在一个简单的案例中分别了解一下这四个组件。
2、分别讲解
参考代码地址:https://github.com/guoergongzi/GMVVMDemo/tree/main
2.1 LifeCycle
在Android代码解耦的过程中,处理生命周期是一个很核心的问题,一个普通组件必须要依赖系统组件(如Activity、Fragment)的调用才能知道自己方法运行的时机,LifeCycle这个组件正是为了解决这个问题而生。
LifeCycle提供了两个接口:LifecycleOwner和LifecycleObserver;只要让实现了LifecycleObserver接口的普通组件去订阅实现了LifecycleOwner的系统组件,就可以让该组件可以感知到系统组件的生命周期,并在对应的时机去处理自身的逻辑。
下图中可以看到,我们现在版本源码中的Activity已经实现了LifecycleOwner,因此我们只要编写LifecycleObserver的实现并让它订阅Activity即可。同理Fragment也实现了这个接口,这里只以Activity做一个演示。
我们编写如下LifecycleObserver实现类:
public class TestClass implements DefaultLifecycleObserver {
@Override
public void onResume(@NonNull LifecycleOwner owner) {
DefaultLifecycleObserver.super.onResume(owner);
Toast.makeText((Context) owner, "测试内容", Toast.LENGTH_SHORT).show();
}
}
在activity的onCreate()方法中用以下代码订阅:
getLifecycle().addObserver(new TestClass());
这样当Activity显示时我们的测试Toast就会显示出来,相当于直接写在Activity的onResume()回调中。
这里只演示了一个简单的Activity中使用Lifecycle的方式,之后的内容中我会详细的分享关于Lifecycle的更多知识,这里我们先接着了解我们的下一个组件。
参考代码Module:glifecycledemo
2.2 ViewModel
ViewModel是Google提供的Android端实现MVVM中的VM层的标准方式,它在创建时需要一个LifecycleOwner作为参数,在这个LifecycleOwner销毁时它自己也会销毁(Activity旋转造成的Activity销毁和重建并不会触发)。
把逻辑放在ViewModel中既可以给Activity(或Fragment等其它LifecycleOwner)瘦身,又能避免Activity旋转造成数据丢失。
下面我们用一个小案例来了解一下ViewModel的基本使用方式,首先先创建一个ViewModel子类,写一些逻辑在里面。
public class TestViewModel extends ViewModel {
private Timer timer;
private int timeCount = 0;
public void startTimer() {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
timeCount++;
Log.v("G", "timeCount = " + timeCount);
}
};
timer = new Timer();
timer.schedule(timerTask, 1000, 1000);
}
public int getTimeCount() {
return timeCount;
}
@Override
protected void onCleared() {
super.onCleared();
timer.cancel();
}
}
然后在Activity的onCreate中写下以下代码来绑定Activity和ViewModel。
TestViewModel viewModel = new ViewModelProvider(NewActivity.this).get(TestViewModel.class);
viewModel.startTimer();
我们可以再写一个按钮来测试getTimeCount()方法。
Button timeButton = findViewById(R.id.btn_view_model_demo);
timeButton.setOnClickListener(view -> {
Toast.makeText(NewActivity.this, "计时到第" + viewModel.getTimeCount() + "秒", Toast.LENGTH_SHORT).show();
});
这样我们点击按钮时就可以看到Activity正确的获取到ViewModel中定义的值了。
在项目运行过程中,如果我们旋转一下手机画面,我们会发现timeCount依然在之前的数值上累积,而没有因为旋转归零,这点验证了ViewModel可以避免Activity旋转造成的数据丢失。我们退出Activity,会发现我们写在计时器里面的日志停下来了,说明Activity的销毁可以销毁ViewModel并触发它的onCleared()方法。
这里用一个简单的例子介绍了ViewModel,和Lifecycle一样,ViewModel还有很多用法和知识需要我们了解,但是我们把这部分也放在之后的文章中。另外我们还没有了解LiveData和DataBinding,接下来我们了解了这两个框架以后,我们会发现这个Demo里的ViewModel有更好的写法。
参考代码Module:gviewmodeldemo
2.3 LiveData
LiveData是一种可观察的数据存储器类,相比于其它可观察类,它拥有感知生命周期的作用,确保它仅在应用组件处在活跃中时才会被触发。
我们把ViewModel部分的案例稍加改造,用MutableLiveData来包装timeCount参数,通过setValue()或postValue()方法来更新数值。
public class TestViewModel extends ViewModel {
private Timer timer;
private MutableLiveData<Integer> timeCount = new MutableLiveData<>();
{
timeCount.setValue(0);
}
public void startTimer() {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
timeCount.postValue(timeCount.getValue() + 1);
}
};
timer = new Timer();
timer.schedule(timerTask, 1000, 1000);
}
public Z<Integer> getTimeCount() {
return timeCount;
}
@Override
protected void onCleared() {
super.onCleared();
timer.cancel();
}
}
然后在Activity的onCreate()中添加观察者并找一个TextView来显示timeCount的值。
TestViewModel viewModel = new ViewModelProvider(MainActivity.this).get(TestViewModel.class);
viewModel.startTimer();
TextView textView = findViewById(R.id.tv_live_data_demo);
LifecycleOwner owner = MainActivity.this;
viewModel.getTimeCount().observe(owner, integer -> {
textView.setText("计时到第" + integer + "秒");
Log.v("G", "timeCount = " + integer);
});
运行起来后我们会发现,TextView的内容会不停的刷新,这就是LiveData的主要作用——在数值更新时通知注册的观察者。
到这里为止,LiveData看起来和EventBus之类的事件通知框架区别不大,那么为什么我们要在MVVM中使用这个组件呢?核心原因有几点:
1、LiveData在Activity等LifecycleOwner不活跃时不会发布通知,上图的案例中我们把日志打印的代码从计时器中移到了观察者回调中,我们把Activity退到后台时会发现日志不再打印了,这可以避免我们界面不可见时依然处理事件造成手机运行资源的浪费。
2、LiveData和Activity生命周期同步,不易发生内存泄漏,也不用再界面销毁时处理它。
3、我们的代码中声明timeCount对象时用了MutableLiveData类,提供get方法时却返回了LiveData类,MutableLiveData类是LiveData类的子类,它们的区别是MutableLiveData可以调用postValue()或setValue()方法更新内容,LiveData类却不行。从架构的角度看,这种方式保证了数据只会在VM层更新,不会被View层更新——因为View层通过get方法得到的是无法更新的LiveData类。
4、LiveData和我们之后要介绍的DataBinding可以实现配合。
可以看到,我们订阅LiveData时需要提供一个LifecycleOwner对象,就是这个参数让LiveData有了感知Activity生命周期的能力。我们在进行MVVM开发时有时候并不会直接用到LifeCycle,但它是支持Android端MVVM实现的重要组件,这也是这篇文章要介绍它的原因。
参考代码Module:glivedatademo
2.4 DataBinding
到目前为止,MVVM的核心特征——Model改变时数据自动变化我们仍然没有看到。当我们想要在textView中显示内容时,还是要在观察者模式里手动的调用setText,没错,这就是DataBinding这个组件为我们解决的问题。
首先我们要在module目录下的build.gradle文件中添加以下代码,允许项目使用Databinding
android {
。。。
// 允许项目使用databinding
dataBinding {
enabled = true
}
}
然后我们更改一下上一个Demo中的TestViewModel,把返回的LiveData类型改成拼接好的字符串timeCountString:
public class TestViewModel extends ViewModel {
private Timer timer;
private final MutableLiveData<String> timeCountString = new MutableLiveData<>();
private int timeCount = 0;
{
timeCountString.setValue("计时到第" + timeCount + "秒");
}
public void startTimer() {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
timeCount += 1;
timeCountString.postValue("计时到第" + timeCount + "秒");
}
};
timer = new Timer();
timer.schedule(timerTask, 1000, 1000);
}
public LiveData<String> getTimeCount() {
return timeCountString;
}
@Override
protected void onCleared() {
super.onCleared();
timer.cancel();
}
}
接下来我们要把界面的布局文件特殊处理一下:
<?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:tools="<http://schemas.android.com/tools>">
<data>
<variable
name="vm"
type="com.gegz.gdatabindingdemo.TestViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@{vm.timeCount}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
可以看到,我们的布局文件和不使用DataBinding时的样子有很大不同:
1、我们用layout包裹了原本最外层的布局,并且在原本最外层布局的同级添加了一个data标签,在其中用添加了一个name为vm、type为TestViewModel的variable标签。
2、我们没有给TextView设置Id,取而代之的是我们直接给它设置了text属性,并且在属性里使用了@{vm.timeCount}的写法。
最后我们在MainActivity的onCreate中加载ViewModel和DataBinding:
ActivityMainBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mBinding.setLifecycleOwner(this);
TestViewModel viewModel = new ViewModelProvider(this).get(TestViewModel.class);
viewModel.startTimer();
mBinding.setVariable(BR.vm, viewModel);
这里有几个需要注意的地方:
1、我们调用了DataBindingUtil的setContentView就不用再调用Activity的setContentView了,如果我们点进去看看源码,就会发现DataBindingUtil的setContentView调用了Activity的这个方法。
2、我们这里用到了一个ActivityMainBinding,但我们并没有声明它,不用担心,这个类是系统自动生成的。
3、我们给ActivityMainBinding的对象设置了viewModel作为数据源,这个地方也可以用普通的实体类,但使用ViewModel可以更好的解耦逻辑层的代码,并且能灵活的处理一些逻辑。
参考代码Module:gdatabindingdemo
3、总结
到最后一个Demo为止,我们已经把上面提到过的四个组件——LifeCycle、ViewModel、LiveData和DataBinding都用上了,并且了解了这四个组件各自扮演的角色。
结合我们文章开头对MVVM架构的介绍来分析我们这个Demo,会发现Demo中的Activity只负责将DataBinding、ViewModel和自己绑定起来,TestViewModel类中包含了我们这个界面的逻辑——计时并更新数据,而xml文件则通过框架的支持完成了界面的显示和更新,View层和ViewModel层之间的分工体现的十分明确。有一个遗憾是我们还没有一个独立的Model层,毕竟我们这个Demo的功能还太过简单,给它添加一个单独的Model层难免有过度设计之嫌,我们会在这个系列之后的文章中演示Model层的写法。
注:出于篇幅考虑,本文中很多代码片段不完整,大家可以从Github上下载Demo学习。
代码地址:
参考文档:
知乎:MVC、MVP、MVVM -- 可多C
CSDN:三种架构模式——MVC、MVP、MVVM -- 非早起选手
CSDN:Android LifeCycle详解 -- 优雅的心情
CSDN:Android ViewModel详解 -- 赵彦军
CSDN:Android LiveData 详解及使用 -- 大肠包小肠|
CSDN:Android DataBinding的基本使用 -- 尹中文
CSDN:Android DataBinding 从入门到进阶,看这一篇就够 -- 程序员一东