Android端MVVM从入门到实战(第一篇) - MVVM和四大官方组件

1、MVVM简介

MVVM是相对于MVC和MVP的一个概念,是一种架构模式。

1.1 MVC

传统的MVC中,View改变通知Controller进行处理,Controller处理结束后通知Model层更新,Model层更新以后通知View层渲染,指令单项流动,角色分工明确。但是MVC有三个缺点,1、三个角色互相持有对方依赖,因此很难复用其中任意一方;2、开发时必须三个模块同步开发,否则很难相互匹配;3、由于每一个角色的改变都会直接或间接的影响另外两个角色,所以任何改动都必须考虑全盘影响。

Untitled.png

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层进行适配即可,不会再影响到对方,降低了修改代码时的影响范围。

Untitled 1.png

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提供的组件进行处理。

Untitled.jpeg

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做一个演示。

Untitled 2.png

我们编写如下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层的写法。

在这篇文章里,我们已经对android端的MVVM有一个基本的理解,并且实现了一个简单的小界面,但是很明显,这种程度的理解还远远不够我们去完成实际项目,我们还需要学很多知识,比如怎么加载网络图片,比如怎么编写fragment和recyclerView,比如我们之前提到过的Model层怎么设计,这些我们都会在之后的文章中介绍,希望大家多多点赞收藏,期待在下一篇文章中与大家讨论更详细的Android端MVVM知识。

注:出于篇幅考虑,本文中很多代码片段不完整,大家可以从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 从入门到进阶,看这一篇就够 -- 程序员一东

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容