
前言
在开发模式的演进过程中 MVC,MVP,MVVM一一登上舞台。但是并不意味着MVVM一定就比MVC MVP优秀。不同的项目有不同的体量,开发中要根据项目体量选择合适的开发模式。
市面上介绍mvvm的项目不在少数,但是看了很多,都在介绍源码原理,开发中的踩坑过程,而且有的是过时的资料,却很少见到能够直接从项目需求入手帮助不熟悉MVVM的开发者从入门到熟悉原理,再到框架优化的。这个空缺我来补充,如果提及到一些其他原理性的东西不便长篇展示,我会引用链接,或者引用他文的原文加以说明。
特别说明: 以下所有信息都基于截止到 2020年6月2日10点23分的最新官方资料和源码版本。如果存在任何历史版本的差异,本文不会过分纠结。正文大纲
- 有关MVVM的几个重要组件
- ViewModel
- DataBinding
- LiveData
- LiveDataBus
- 案例实操
- MVVM的优缺点
正文
有关MVVM的几个重要组件
ViewModel
ViewModel 是androidx包中抽象类。它是谷歌开放给全世界开发者用来改善项目架构的一个组件。

既然是探索ViewModel的本源,那就从它的官方注解开始吧。

ViewModel是一个准备和管理Activity和Fragment的数据的类。它也可以掌控Activity、Fragment和应用中其他部分的通讯。 一个ViewModel总是关联到一个域(Activity或Fragment)被创建。并且只要域是存活的,ViewModel就会一直被保留。比如。如果域是一个Activity,ViewModel就会存活,直到Activity被finish。 换句话说,这意味着,如果它的持有者由于配置改变而被销毁时(比如屏幕旋转),ViewModel并不会被销毁。新的持有者实例,将会仅仅重新连接到已经存在的ViewModel。 ViewMode存在的目的,就是为Activity/Fragment 获得以及保留 必要信息。Activity / Fragment 应该可以观察到VIewModel的变化,ViewModel通常通过LiveData或者DataBinding 暴露信息。你也可以你自己喜欢的使用可观察的结构框架。 ViewModel仅有的职责,就是为UI管理数据,它不应该访问到你任何的View层级 或者 持有Activity 、Fragment的引用。谷歌爸爸其实已经把意思讲的很明白,上面一段话中有几个重点:
ViewModel 唯一的职责 就是 在内存中保留数据
多个Activity或者Fragment可以共用一个ViewModel
在屏幕旋转时,ViewModel 不会被重建,而只会连接到重新创建的Fragment/Activity
使用ViewModel有两种方式,LiveData或者
DataBinding(或者你可以自定义观察者模式框架),用他们来暴露ViewModel给V层
核心功能
ViewModel的核心功能:在适当的时机执行回收动作,也就是 onCleared() 函数释放资源。而这个合适的时机,可以理解为 Activity销毁,或者Fragment解绑。借用一张图来解释,就是:

基本用法
创建一个新的项目,定义我们自己的UserModel类,继承ViewModel:import android.util.Logimport androidx.lifecycle.ViewModelclass UserModel : ViewModel() { init { Log.d("hankTag", "执行ViewModel必要的初始化") } override fun onCleared() { super.onCleared() Log.d("hankTag", "执行ViewModel 清理资源") } fun doAction() { Log.d("hankTag", "执行ViewModel doAction") }} 在View层使用定义好的ViewModel:import androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport androidx.lifecycle.ViewModelProviderclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 获取ViewModel对象 val userModel = ViewModelProvider(this).get(UserModel::class.java) // 使用ViewModel对象的函数 userModel.doAction() }}就这么简单,运行程序能看到日志:

同时ViewModelProvider也支持两个参数的构造函数,除了上面的owner=this之外,还可以传入另一个Factory参数。
如果不传入这个Factory,源码中会在拿到ViewModel的class对象之后通过无参构造函数进行反射创建对象。但是如果ViewModel要用有参构造函数来创建的话,那就必须借助Factory:// ViewModelclass UserModel(i: Int, s: String) : ViewModel() { var i: Int = i var s: String = s init { Log.d("hankTag", "执行ViewModel必要的初始化") } override fun onCleared() { super.onCleared() Log.d("hankTag", "执行ViewModel 清理资源") } fun doAction() { Log.d("hankTag", "执行ViewModel doAction: i = $i, s : $s") }}// ViewModelFactoryclass UserModelFactory(val i: Int, val s: String) : ViewModelProvider.Factory { override fun ViewModel?> create(modelClass: Class): T { return modelClass.getConstructor(Int::class.java, String::class.java).newInstance(i, s) }}// View层class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 获取ViewModel对象 val userModel = ViewModelProvider(this, UserModelFactory(1, "s")).get(UserModel::class.java) // 使用ViewModel对象的函数 userModel.doAction() }} 运行结果:06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel必要的初始化06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel doAction: i = 1, s : s06-02 11:20:57.836 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel 清理资源核心原理
源码探索的目标是,ViewModel是如何感知Activity的生命周期清理自身资源的。其实也就是看 onCleared函数是如何被调用的。ViewModelProvider(this).get(UserModel::class.java)上面这句代码是是用来获得ViewModel对象的。这里分为2个部分,其一,ViewModelProvider的构造函数:
上面标重点的注释的意思是:创建一个ViewModelProvider,这将会创建ViewModel并且把他们保存到给定的owner所在的仓库中。这个函数最终调用了重载的构造函数:
这个构造函数有两个参数,一个store,是刚才通过owner拿到的,一个是,Factory。store顾名思义,是用来存储ViewModel对象的,而Factory的意义,是为了通过class反射创建对象做准备的。使用构造函数创建出一个ViewModelProvider对象之后,再去 get(UserModel::class.java)
通过一个class对象,拿到他的canonicalName全类名。然后调用重载get方法来获取真实的ViewModel对象。
这个get函数有两个参数,其一,key,字符串类型。用于做标记,使用的是一个定死的字符串常量DEFAULT_KEY拼接上modelClass的全类名,其二,modelClass的class对象,内部代码会使用class进行反射,最终创建出ViewModel对象。上面提到了一个重点:Store仓库,创建出来的ViewModel都会被存入owner所在的仓库。那么,阅读仓库的源码:
那么一个Activity,它作为ViewModelStoreOwner,他自己的viewModelStore何时清理
答案是:onDestroy() . 但是这里有一个特例,配置改变,比如屏幕旋转时,ViewModelStore并不会被清理。并且,Fragment的源码中也有类似的调用:
总结
ViewModel的核心,是自动清理资源。我们可以重写onCleared函数,这个函数将会被ViewModel所在的Activity/Fragment 执行onDestory的时候被调用,但是当屏幕旋转的时候,并不会清理。在ViewModel的架构中,有几个关键类,ViewModelProvider 用于获取ViewModel
ViewModelStore 用于存储ViewModel
ViewModelStoreOwner 用于提供ViewModelStore对象,Activity和Fragment都是ViewModelStoreOwner 的实现
ViewModelProvider的内部类Factory,用于支持ViewModel的有参构造函数,毕竟ViewModel对象是通过class反射创建出来的,需要支持默认无参,以及手动定义有参构造函数
DataBinding
DataBinding,单词意思: 数据绑定,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。MVVM 相对于 MVP,其实就是将 Presenter 层替换成了 ViewModel层。DataBinding 能够省去我们一直以来的findViewById() 步骤,大量减少Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常. DataBinding:- 支持在java代码中不用findViewById来获取控件,而直接通过DataBinding对象的引用即可拿到所有的控件id
- 进行数据绑定,使得ViewModel(数据)变化时,View控件的属性随之改变
- 支持 数据的双向绑定,改变View控件的属性,那该属性绑定的ViewModel(数据)随之改变
- 支持将任何类型的ViewModel绑定到View控件上,包括系统提供的类型(包括基础类型和集合类型),以及自定义的类型
- 支持特殊的View属性,比如ImageView的图片源,可以自定义图片加载的具体过程(Glide...)
- 支持在xml中写简单的表达式, 比如函数调用,三元表达式...
- 支持对res资源文件的引用,比如 dimen,string...
- 支持与 LiveData(下文会解释概念)合作,让数据的变动 关联 Activity / Fragment 的 生命周期
别多想,就是纯粹的数据,不涉及到生命周期) 和 视图。而MVVM的核心是ViewModel抽象类,核心功能是感知持有者Activity/Fragment的生命周期来释放资源,防止泄露。我们使用DataBinding,创建封装数据类型,也不用继承ViewModel抽象类。至于ViewModel抽象类的注释上为什么这么说,我也是很费解。但是看了许多DataBinding的资料,项目,包括在自己的项目中使用DataBinding之后,它给我的感受就是:很糟糕。没错,糟透了,也许是因为时代进步了,也许是因为我的代码洁癖,DataBinding放入我的代码,我总感觉有一种黏乎乎的感觉,就和最早的JSP一样,一个HTML文件中,混入了HTML标签,js代码,以及 java代码,尽管我承认DataBinding的功能很强大,但是使用起来确实不舒服。有一些老代码如果大量使用了这种写法,我们了解一些DataBinding核心原理也是有必要的。核心功能
DataBinding的核心功能是:支持View和数据的单向或者双向绑定关系,并且最新版源码支持 setLifecycleOwner 设置生命周期持有者。基本用法
在所在module的build.gradle文件中,找到androd节点:插入以下代码来开启DataBindingdataBinding{ enabled true} 改造布局xml,使用标签包裹原来的布局,并且插入 节点<?xml version="1.0" encoding="utf-8"?> type="androidx.databinding.ObservableMap" /> type="androidx.databinding.ObservableList" /> name="userBean" type="com.zhou.databinding.UserBean" /> name="map" type="ObservableMap, Object>" /> name="title" type="java.lang.String" /> xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> android:id="@+id/tvTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@={title}" android:textSize="30sp" tools:text="title:" /> android:id="@+id/tvTitle2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{title}" android:textSize="30sp" tools:text="title:" /> <...> 这里支持import操作,类似java的import导包,导入之后就能在@{} 中使用引入之后的函数和类. 如果想双向绑定,就使用@={}。Varilable标签是用来定义数据的,name随意,字符串即可。type必须是封装类型的全类名,支持泛型实例。 Java/kotlin 代码层面: 数据的绑定支持几乎所有类型,包括jdk,sdk提供的类,或者可以自定义类:class UserModel { val user = User("hank001", "man")} 对,这里命名为UserModel,但是它和androidX里面的抽象类ViewModel没有半毛钱关系。 在Activity中,需要使用DataBindingUtil将当前activity与布局文件绑定。class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.lifecycleOwner = this binding.title = "asfdsaf" val map = ObservableArrayMap<String, Any>().apply { put("count", 0) } binding.map = map binding.userBean = UserBean() Thread { for (i in 0..100) { binding.title = "asfdsaf$i" // 数据变更时,UI会发生变更 map["count"] = "count$i" Thread.sleep(10) } }.start() }} 上面的代码,如果运行起来,
可以看到我并未主动去使用textview的引用去操控它的text属性。这些工作都是在databinding框架中完成的。至于更具体更复杂的用法,本文不再赘述。网上很多骚操作。核心原理
核心功能是 数据绑定,也就是说,只要知道了databinding是如何在数据变化时,通知到view让它改变属性的,databinding的秘密就算揭开。直接从代码进入源码。这一切的源头,都是由于我们使用了DataBindingUtil来进行绑定引起的。那么就从它开始。binding = DataBindingUtil.setContentView(this, R.layout.activity_main)binding.title = "asfdsaf"
注释的大意是:将Activity的内容View设置给 指定layout布局,并且返回一个关联之后的binding对象。指定的layout资源文件不能是merge布局。
随后该函数调用到了:

继续追踪bind函数:
目标转移到了sMapper.getDataBinder(),进去看了之后发现是抽象类,找到他的实现类:
结果发现了我自己的包名,看到这里应该有些明白了,我并没有写这个DataBindingMapperImpl类,它只能是as帮我们自动生成的。
所以,绑定View和数据的具体代码应该在这个类里面有答案,经过追踪,发现代码走到了这一行:
回到一开始,binding = DataBindingUtil.setContentView(this, R.layout.activity_main) 这句话,饶了一大圈,最终得到了一个ActivityMainBindingImpl对象,随后我们用这个对象去操作view引用来绑定数据binding.title = "asfdsaf" 那就直接从这个title看起,上面是kotlin的setTitle写法,直接看setTitle方法:
其实它就是把xml布局文件中的 title属性值设置为 传入的形参值。然后 notifyPropertyChanged(BR.title): 通知 ID为 BR.title的属性值发生了改变。
直接进到了观察者模式Observable接口的一个实现类BaseObservable,由于as的原因,代码无法继续索引(它会直接跳到xml文件),但是经过debug,我发现,当title发生变化时,
从上面的命名可以看出,DataBinding框架应该是给每一个xml中定义的变量variable都建立了一个独立的监听器,在variable发生变化时,这个监听器会在 variable 发生改变时,通知界面元素发生属性变更。,查找这个监听器的调用位置 executeBinding()函数,结果有了意外发现,“双向数据绑定”的原理也被揭开。
这里传了3个参数,BeforeTextChanged,OnTextChanged,AfterTextChanged,刚好对应了TextWatcher接口中的3个方法。进入看一眼上面的setTextWatcher():
name="title" type="java.lang.String" /> 这一种定义databinding变量的方式。如果是map类型,map的内部元素发生变更,UI也是可以随之更新的,又是怎么回事呢? name="map" type="ObservableMap, Object>" /> 经过一番追踪,发现有点不同。map的用法有点不同: android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{String.valueOf(map.count)}" android:textSize="30sp" tools:text="userBean:" /> 当我在activity中变更map的元素值的时候,
会执行到setMap方法:
当map的某一个元素值发生变化时,会执行到handlerFieldChange,
随后 onFieldChange函数,
如果确认发生变更,就会requestRebind()重新去绑定最新的map对象。 补充说明一点:binding.lifecycleOwner = this 这句代码,
如果一个DataBinding对象的mLifeCycleOwner不是空,那么:
在绑定数据的时候,就会去判定当前mLifeCycleOwner是不是STARTED之后,如果不是,数据的绑定都不会执行。总结
综合以上所有小结论,总结一下:- 在layout中定义的每一个被使用的variable都会建立独立的监听器,没有被用到的,并不会有监听
- 数据的单项绑定:数据变化,通知View设置属性,是通过观察者模式设置监听器来实现,具体的监听方式和variable的类型有关
- 数据的双向绑定:其实是通过 textWatch接口来实现,当view的text属性发生改变,会通知到该view已经绑定的监听器更新数据
- DataBinding提供了很多可观察的类型, 如 ObservableMap,ObservableList等,如果是集合类型,必须用这个,才能实现内部元素的变动对接UI的变动。自定义类型可以继承BaseObservable并且重写get set方法来实现同样的绑定效果(具体如何做,这里不赘述)。
- DataBinding的mLifeCycleOwner属性可以让数据绑定感应Activity/Fragment的生命周期,防止内存泄漏.
技术直播
Live Class


↑点击直接进入课堂↑
是兄弟,就来 “kan” 我