漫谈 MVVM(1)ViewModel_DataBinding 核心原理 (2)
<img src="漫谈 MVVM.assets/image-20200530155155198.png" alt="image-20200530155155198" style="zoom:67%;" />
既然是探索 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 解绑。
借用一张图来解释,就是:
在整个 Activity 还处于存活状态时,ViewModel 都会存在。而当 Activity 被 finish 的时候,ViewModel 的 onCleared 函数将会被执行,我们可以自己定义函数内容,清理我们自己的资源,在 Activity 被销毁之后。该 ViewModel 也不再被任何对象持有,下次 GC 时它将被 GC 回收。
基本用法
创建一个新的项目,定义我们自己的 UserModel 类,继承 ViewModel:
import android.util.Logimport androidx.lifecycle.ViewModel
class 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.ViewModelProvider
class 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()}}
就这么简单,运行程序能看到日志:
<img src="漫谈 MVVM.assets/image-20200602104022542.png" alt="image-20200602104022542" style="zoom: 100%;" />
同时 ViewModelProvider 也支持两个参数的构造函数,除了上面的 owner=this 之外,还可以传入另一个 Factory 参数。
如果不传入这个 Factory,源码中会在拿到 ViewModel 的 class 对象之后通过无参构造函数进行反射创建对象。但是如果 ViewModel 要用有参构造函数来创建的话,那就必须借助 Factory:
// ViewModelclass UserModel(i: Int, s: String) : ViewModel() {
var i: Int = ivar 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 = s")}
}
// ViewModelFactoryclass UserModelFactory(val i: Int, val s: String) : ViewModelProvider.Factory {override fun <T : ViewModel?> create(modelClass: Class<T>): 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 何时清理?
![
image-20200602114123983](漫谈 MVVM.assets/image-20200602114123983.png)
答案是: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 的 生命周期
从 ViewModel 的注释中我们得知,DataBinding 是向 View 层暴露 ViewModel 的一种方式。但是事实上并非如此,DataBinding 只是数据绑定,它和 ViewModel 抽象类没有半毛钱关系。DataBinding 绑定的双方:是 数据(别多想,就是纯粹的数据,不涉及到生命周期
) 和 视图。而 MVVM 的核心是 ViewModel 抽象类,核心功能是感知持有者 Activity/Fragment 的生命周期来释放资源,防止泄露。我们使用 DataBinding,创建封装数据类型,也不用继承 ViewModel 抽象类。至于 ViewModel 抽象类的注释上为什么这么说,我也是很费解。但是看了许多 DataBinding 的资料,项目,包括在自己的项目中使用 DataBinding 之后,它给我的感受就是:很糟糕。没错,糟透了,也许是因为时代进步了,也许是因为我的代码洁癖,DataBinding 放入我的代码,我总感觉有一种黏乎乎的感觉,就和最早的 JSP 一样,一个 HTML 文件中,混入了 HTML 标签,js 代码,以及 java 代码,尽管我承认 DataBinding 的功能很强大,但是使用起来确实不舒服。有一些老代码如果大量使用了这种写法,我们了解一些 DataBinding 核心原理也是有必要的。
核心功能
DataBinding 的核心功能是:支持 View 和数据的单向或者双向绑定关系,并且最新版源码支持 setLifecycleOwner 设置生命周期持有者。
基本用法
在所在 module 的 build.gradle 文件中,找到 androd 节点:插入以下代码来开启 DataBinding
dataBinding{enabled true}
改造布局 xml,使用**<layout></layout>**标签包裹原来的布局,并且插入<data>节点
<?xml version="1.0" encoding="utf-8"?><layout><data><import type="androidx.databinding.ObservableMap" /><import type="androidx.databinding.ObservableList" />
<variablename="userBean"type="com.zhou.databinding.UserBean" /><variablename="map"type="ObservableMap<String, Object>" />
<LinearLayout 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">
这里支持 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: ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = DataBindingUtil.setContentView(this, R.layout.activity_main)binding.lifecycleOwner = thisbinding.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 = "asfdsafi"
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 布局。
随后该函数调用到了:
这里首先使用 activity.setContentView,将 layoutId 设置进去,常规操作。然后,拿到 activity 的 decorView,进而拿到 contentView,随后调用 bindToAddViews。
评论