DataBining从基本使用到核心源码解析,看不懂的兄弟跟我一起去摆摊

前言

DataBinding是GOOGLE发布的jetpack库里的重要一员,通过使用DataBinding来实现MVVM架构,可以有效避免了MVP架构里新建文件过多的繁杂问题,并且每次数据源更新时不再需要开发人员来调用控件的set方法更新数据了,同时支撑双向绑定也能让控件之间互相刷新,可以减少控件之间的监听,从而减缓陷入回调地狱的进度。今天笔者给大家分享一下DataBinding从入门基本使用,到深入源码的一整个过程。如果你从来没用过DataBinding,那么恭喜你看完就是jetpack大佬了。哈哈开个玩笑乐呵乐呵,Jetpack深似海,不研究个三五年是不可能精通的,本文只是Jetpack中的沧海一粟而已,不过笔者真心希翼看完能对大家有所帮助。

一、导包

二、基本用法

方式1、每次都手动设置数据

步骤一 首先定义好一个普通的JavaBean,这就是控件需要的数据源,不需要做任何的特殊处理。

步骤二 修改布局成databinding布局

进入activity的布局文件,鼠标放到根布局上,然后同时按住alt+enter打开系统提示框。由于大家之前在build.gradle中设置了打开dataBinding,所以当前会出现Convert to data binding layout选项,顾名思义就是转换成dataBinding支撑的布局模式,选择该选项即可。

选择好了该模式以后,布局文件大概就会变成这样子。布局最外层给自动加上了layout便签,包裹了大家最初的根布局。其次,系统自动生成了和之前的根布局同级。

接下来只需要在data标签里面配置好系统自动生成类的类名,以及<variable style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;">属性即可。其中variable中name有两个作用,第一是使用该数据源,第二个是用来自动生成更新数据时调用的方法名,type指定好前面大家写好的那个bean即可。在控件上使用时只需要通过@{name.field}的方式设置在控件上即可,当然这里以TextView为例,ImageView需要特殊处理,后面会讲到。

经过以上的准备工作,接下来就要开始正式使用了。我这里打开了一个线程,然后每过一秒变化一下数据源,注意每次修改了数据源,最后都需要调用setTestVariable方法才能生效。

运行效果如下:

效果符合预期,每过一秒就自动更新数据,而大家并没有对TextView进行setText操作。

大家成功地实现了在数据源变化的时候自动设置到ui上面,也仅仅是实现了功能而已,但是这种方法非常地笨拙,每次数据源更新的时候还要给dataBinding重新设置数据源,操作很麻烦,那有没有只需要写一遍就可以初始化数据源,从而一劳永逸的办法呢?答案是有的,接下来,大家来看看方式2。

方式2、利用@Bindable注解以及官方的notifyPropertyChanged方法来实现

xml文件不需要变,按照方式1的写法就可以了,只需要在Bean的字段的get方法上面加上@Bindable注解,然后在字段的set方法里调用一下notifyPropertyChanged方法即可,需要注意的是,必须要继承BaseObservable才会有该方法。其中@Bindable是告诉DataBinding在可以调用getName方法来获取name的值,notifyPropertyChanged方法是告知DataBinding当前数据已修改,快去调用get方法获取最新数据吧!所以不用重新设置数据源的原因大家也都看出来了,其实就是在给Bean设值的时候通知了DataBinding,然后DataBind自动去更新了。

接下来在使用时,就不需要重新给DataBinding设置数据源了 。
image

当时这种方式也有缺陷,就是Bean里面每个字段的set和get方法都需要进行修改,加上@Bindable和notifyPropertyChanged方法,否则将会自动更新失败。只要是需要人工修改的地方,那么在实际业务中不可避免地可能出现问题。那么有没有容错性更强的方式呢,当然是有的,下面说下方式3,也是最常用的方式。

方式3、使用ObservableField的方式

修改Bean即可,将属性包装成ObservableField,该属性的类型改成泛型的形式给出,如下图

在使用时的方式也需要稍加修改,需要先获取到ObservableFiled属性,然后调用其set方法来设置新值。这种方法是不是比前面那2种要好不少呢,既不需要每次赋值的时候给DataBinding赋值,也不需要记得给Bean里每个字段的get方法加上@Bindable,以及set方法上手动加上调用notifyPropertyChanged方法,这种方式也是个人觉得最好用的方式。

三、高级用法

1、数据源是Map或者List时如何使用

如果数据源是存放在Map或者List时,DataBinding提供了相应封装好的类可以直接使用,Map -> ObservableMap,List -> ObservableList,不过这两个都是接口,实例化的时候用ObservableArrayMap和ObservableArrayList即可。

大家来简单尝试一下Map进行存值和使用吧,首先在Bean中添加一个数据源为ObservableMap类型。

这里还需要相应添加一个key,因为java的字符串是不支撑单引号和双引号一起使用的,在xml中使用时需要。

接下来就是对map进行赋值了,这里需要注意,map和key需要同时设置,不然在xml中绑定了是拿不到值的。

2.在xml中进行运算符操作

在使用了DataBinding的xml中可以使用简单运算符操作,比如,大家可以用取到的数字源进行操作后再设置到控件上,这里举一个例子,大家在获取的值后面加一个单位。需要注意的是,在这里拼接字符串需要使用的并不是单引号,而是数字1左边的那个键。

除了上面用到的字符串拼接以外,还支撑另外一些操作,具体见下图,注意并不是所有的符号都支撑。

3.双向绑定

什么是双向绑定?

前面的实现都是单向的绑定操作,也就是说数据源改变以后会自动更新到控件上。但是,如果想让控件的值被用户手动输入变化后也能自动地同步到数据源里,这个操作就叫做双向绑定。

双向绑定以后可以实现什么效果?

为了演示一下双向绑定的效果,我给页面上加了一个输入框以及一个文本框,它们都使用的同一个数据源。当用户给输入框删除或者输入新数据以后,这二者虽然使用的同一份数据源,但是由于输入框输入的内容并没有同步到数据源中,所以文本框的数据并不会自动地刷新成用户在输入框中输入的值。

而双向绑定以后是这样的,输入框不管是删除还是输入,都会马上自动把结果同步到文本框上

如何实现双向绑定呢?

双向绑定具体做法是将@{}改成@={}。如下图,在将EditText的值与数据源双向绑定了以后,当用户在输入框中输入内容以后,由于输入框的值会同步到二者共用的数据源中,下面的TextView内容就会自动跟着变化。

4.通过BindingAdapter来自定义Setter

有些控件的属性并不是直接显示在控件上的,而是需要经过处理甚至是第三方API的处理才能使用。又或者是想要覆盖系统自身的属性,也可以,总之就是这里定义的对属性的BindingAdapter注解处理方式会覆盖系统的方式。

比如想要将url设置到ImageView上,需要通过Picasso或者Glide来将url下载转成bitmap来设置,这个时候就需要特殊处理。

首先在Bean里新增一个String类型的url字段,然后在ImageView上自定义一个url属性,将其绑定到url字段上。也可以定义多个,这里多定义了一个error属性。

最后处理一下实际的将url设置到ImageView上的操作

5.设置数据到列表控件中

以RV为例,接下来说一下如何将数据设置到列表控件中,这也是非常常见的操作了。由于列表控件是有Adapter的,相当于设置数据时多传了一层。

这是一个并没有使用DataBinding的简单RecyclerView示例,里面有3种不同布局,当然这里代码写得很不规范,因为这个不是重点。

运行以后大致的效果如下,为了区分三种不同的Item,我将每个Item显示的TextView个数进行了区分,方便大家看懂这是不同的item。

接下来大家要做让它绑定到DataBinding,实现当数据源变化时不调用nofigyDataSetChanged方法来实现Item数据发生变化。

使用步骤参照之前基本使用来即可,第一步新建一个Bean作为DataBinding获取数据库的地方。第二步将那3个item都变成layout包裹的DataBinding布局。

第三步是将当前布局交给DataBinding进行托管

第四步是需要修改一个ViewHolder的实现,由于之前是将View托管到了ViewDataBinding中,所以需要View的时候可以从托管平台DataBinding来获取。这里保存下binding是为了在onBindViewHolder中绑定数据用,要记住现在需要数据都是从ViewDataBiding中获取了,而不能直接从values数组里去获取,不然数据修改了是刷新不了的。这里实现一个接口,是为了偷懒,在onBindViewHolder中统一处理。

最后在onBindViewHolder中调用系统的setVariable方法对DataBinding进行赋值,相比最初的根据position进行判断布局,然后再进行赋值是不是节省了很多代码哈哈,请看下图:

最后,验证一下数据修改时,会自动刷新到rv上吧,我这里开了个线程,每秒修改一下rv上的数据源,注意我只是修改了数据源并没有notifyDataSetChanged。
image

让大家来看一下最终的效果,符合预期。

上源码(划重点)

1、DataBindingUtil是个什么类?

在使用DataBinding的时候,一般第一件事情是初始化DataBinding,代码大概如下:


1.  `finalDataBindingTest viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);`

2.  `finalTestBean testBean = newTestBean();`

3.  `viewDataBinding.setTestVariable(testBean);`

那么DataBinding究竟是何方神圣呢,大家打开该类的属性和方式视图,如下:

这里mapper是调用DataBindingUtil.setContentView初始化DataBinding时用来将layoutid和实际实现类进行映射的一个DataBinderMapperImpl对象,在下面讲解DataBinding初始化的时候还会讲到。sDefaultComponent是预留给开发人员设置的一个属性,默认为null,也会体现在后面DataBinding初始化的过程中。

其中inflate方法是初始化DataBinding方法的另一种方式,在后面给recyclerview上使用DataBinding的例子中会详细讲到,具体代码大概是这样子的:


1.  `ViewDataBinding viewDataBinding = DataBindingUtil.inflate(MainActivity2.this.getLayoutInflater(),R.layout.rv_layout1,parent,false);`

其中bind()方法也和上面的Inflate方法类似,也是用来初始化DataBindingUtil的方式,和setContentView方法依然类似,这三者功能基本是一样的。

下面的findBinding方法和getBinding方法都是对外提供的static方法,用来返回生成的ViewDataBinding类的,通过这些方法大家可以根据View对象或者布局Id来获取绑定好的ViewDataBinding对象。

convertByIdToString后面也会讲到,是系统提供给大家的对外接口,具体很少使用,看注释是有利于打日志。

bindToAddedViews是调用DataBindingUtil.setContentView()进行绑定的时候会调用的中间方法,目的是为了区分当前是不是只有一个控件,进行一些特殊处理,在讲DataBinding初始化的时候也会讲到。

2、DataBinding初始化的时候都做了什么事情

大家在Activity中使用DataBinding的时候,需要进行初始化,代码大致是这么写的:


1.  `DataBindingUtil.setContentView(this, R.layout.activity_main);`

大家点进去看下都做了什么操作,可以看到很简单,首先调用了Activity的setContentView方法,这个是不使用DataBinding的时候也需要调用的,其次是调用了bindToAddedViews方法,调用的时候将最外层的Framlayout容器以及布局的LayoutId给传了进去。

在bindToAddedViews方法中,不管走哪个判断分支,是走到了bind方法,其中第一个参数component默认是null,除非咱们手动去设置。第二个参数是所有view的数组,第三个是布局文件Id。

接着往下面看,是调用到了一个Mapper对象的getDataBinder方法

Mapper对象是一个DataBinderMapper抽象类的子对象,这个抽象类代码如下:

实现了DataBinderMapper抽象类的具体子类是DataBinderMapperImpl类,该类的getDataBinder()方法具体代码如下:


1.  `ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {`

2.  `switch(layoutId){`

3.  `case LAYOUT_AAA:`

4.  `returnnewAaaBindingImpl(component, view);`

6.  `case LAYOUT_BBB:`

7.  `returnnewBbbBindindImpl(component, view)`

8.  `}`

9.  `}`

这段代码十分地简单,就是初始化了相应的DataBinding类,没什么好说的,还有就是保存好了所有的布局文件id、Variable名、到一个map中,一直findUseage你会发现最终是在DataBindingUtil类中的convertByIdToString方法使用,该方法是暴露给开发人员的public方法,说明是预留给大家使用的功能,查看方法提示大概是可以用来和日志有关系?

3、setVariable数据更新流程是怎么样的

首先点进去看下,它是ViewDataBinding该抽象类中的抽象方法。ViewDataBinding类是系统生成的抽象类:

接下来大家看看它的实现类有哪些,这很明显是每个DataBinding的布局文件生成了一个,命名是以布局文件的首字母大写然后驼峰的方式,最后拼上了BindingImpl,当然这个名字是可以自定义的,注意看第二个实现类名不一样,这是因为大家在布局文件中<data style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;">自定义了其名字。

</data>

接下来大家随便点进去一个类吧,查看一下其setVariable方法

发现是调用了setItem1Variable方法,这个方法当然也是自动生成的,是根据variable的name属性生成的。大家可以看到这里首先将数据传给了根据name生成的属性,这就是为了保存起来给后面使用的。

接下来看看从调用notifyPropertyChanged()到最终数据通知到View上的整个流程吧,notifyPropertyChanged方法一直点下去会实行到以下代码:


notifier注册时机是什么?

在这里mNotifier是在setVariable的时候初始化的,下面具体贴一个堆栈就一目了然了 。大家对比一下会发现关于通知类的初始化流程和下面要讲到的callback的注册流程很类似,我后面会手动画一个callback的流程图出来方便大家理解,本文图太多这两个注册的小分支源码就不一步一步地贴图了。

callback注册时机是什么?

在这里callback也是在CallbackRegistry类中添加的,具体也是实行setVariable方法的时候,同时也贴一个堆栈就一目了然了。当然这也是最少要实行一次setVariable的原因之一,如果不实行连回调都没有初始化当然没办法通知。

最后为了方便大家理解这两个注册流程,呈上我给大家画的跳转流程图:

callback注册流程:

明白了mNotifier和callback这两个关键对象的初始化时机,接下来再看看具体是怎么通知的吧。前面看到在notifyPropertyChanged()方法实行时会调用到


1.  `mNotifier.onNotifyCallback(mCallbacks.get(i), sender, arg, arg2);`

实行这行代码就相当于实行到了

继续往下面看

发送了一个handler,那handler接收以后做了什么操作呢?

handler实行到的方法是mRebindRunnable.run()方法。

接下来大家看看这个Runnable的具体实现,其定义就在当前类里,具体如下:

里面核心的方法是这个executePendingBindings(),然后是走到了executeBindings()方法

executeBindings()是一个抽象方法,接下来大家看下它的实现,正是系统给大家生成的BindingImpl类,是不是很有亲切感呢!

接下来就简单了,大家看下实现类里究竟做了什么呢?

其实就是调用了一下DataBinding里封装好的TextView.setText()方法而已,至此就能通了。

最后来总结一下数据刷新流程,为了让大家更好地看懂,我这里也手动画了个图:

数据刷新流程:

4、BR是个什么类

和BindingImpl类一样,BR也是系统自动生成的类之一。生成的内容极其简单,以项目中所有Variable标签名以及使用到的Bean中字段名来命名生成好了相关静态属性,相当于是提供好Id用来方便地更新数据,我怀疑就是之前那个Keys数组生成的,因为值一模一样。

5、双向绑定为何不会死循环

大家知道,在双向绑定的时候,数据会自动刷新控件,同时控件内容有变化,那数据也会同步更新。那么问题来了,在控件内容变化的时候,刷新数据,刷新完数据控件内容又变化,控件内容变化又刷新数据。。。这样不是进入了死循环了吗?如果你真这么认为,那你也太小看GOOGLE工程师了,人家只用了一行代码便解决了这个问题:

看到没,在DataBinding的setText方法中做了处理,如果文本内容不变不算数据有更新,不会设置到控件上,相当于这一趟流程白跑了而已。

那么看完有同学就要问了,为什么不直接更新呢,为啥要通过Handler绕这么大个圈子?我个人觉得可能跟实际做起来要考虑的因素很多有关,包括当前线程未知等原因,下面也列举一下我找到的相关因素代码吧,不得不佩服GOOGLE工程师代码的健壮性真的是强大。不过要是安卓源码出问题,那没人能评估出损失,严谨一点当然是好。

部分健壮性处理代码如下:

1.需要保证当前View已经被加载才能调用

2.需要兼容API<16的情况

3.需要保证DataBinding已经在UI线程初始化,如果是子线程中则没有更新UI的能力

4.需要保证异步安全,所以在requestBind()时进行了加锁

综合各方面来看,由于各种限制比较复杂,而且还存在可能需要切换线程的情况,所以这里使用了Handler来发送消息的方式。

总结

本次大家从DataBinding的三种基本使用方法,延伸到一些常见的高级用法。然后从源码的角度剖析了DataBinding的主要流程,其中每一张源码图都是笔者debug一步一步截下来的,保证了流程不会有问题。其次最核心的setVariable()流程总结图也是笔者纵观整个流程,一个一个类的画出来的,笔者写完已头昏眼花,不过只要能对大家使用和理解DataBinding有所帮助的话,不管怎么样就都值了,牺牲自己成就他人,咱IT工程师不都是实在人么。关于标题,看不懂本文的就不劝你转行了,毕竟每行都不容易。不过兼职摆摊确实是不错,有想去摆摊的同学欢迎一起探讨哈!