Android-Jetpack笔记-DataBinding

DataBinding即数据绑定,可以实现数据和UI的双向绑定。数据改变时,驱动UI刷新;操作UI时,也可以同步给数据。通常在开发界面时,总有findViewById的重复工作,DataBinding可以免去这些操作。同时,DataBinding还可以直接在xml中绑定数据,免去类似setText的操作,让数据来驱动UI刷新。

Jetpack笔记代码

使用

app/build.gradle中开启:

1
2
3
4
5
android {
dataBinding {
enabled = true
}
}

在布局文件中,将光标定位在根布局,alt+enter,然后convert to data binding layout

布局外层会多出一层layout标签:

1
2
3
4
5
6
7
8
<layout>
<!--数据描述-->
<data>
</data>
<!--布局描述-->
<ScrollView>
</ScrollView>
</layout>

在数据描述内,可以导入类和声明变量:

1
2
3
4
<data>
<import type="com.holiday.jetpackstudy.model.User" />
<variable name="user" type="User" />
</data>

在布局描述内,定义一个TextView并绑定数据:

1
2
3
<TextView
android:id="@+id/tv_name"
android:text="@{user.name}" />

在activity中,通过DataBindingUtil得到binding对象:

1
2
3
void onCreate(Bundle savedInstanceState) {
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
}

其中xml文件名决定了生成的binding的类名,xml文件名+Binding,如activity_main.xml生成ActivityMainBinding.java,然后就可以通过binding对象直接访问到view:

1
mBinding.tvName.setTextColor(xxx);

通过binding对象设置数据,驱动UI刷新:

1
mBinding.setUser(user);

原理

DataBindingUtil.setContentView作为入口跟进去,

1
2
3
4
5
6
7
8
9
10
11
12
//DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(Activity activity,int layoutId,DataBindingComponent bindingComponent) {
//这里设置了布局文件
activity.setContentView(layoutId);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}

//省略调用链:bindToAddedViews -> bind

static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}

来到MergedDataBinderMapper.java

1
2
3
4
5
6
7
8
9
10
11
//MergedDataBinderMapper.java
@Override
public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,int layoutId) {
for(DataBinderMapper mapper : mMappers) {
ViewDataBinding result = mapper.getDataBinder(bindingComponent, view, layoutId);
if (result != null) {
return result;
}
}
return null;
}

那么mMappers的值是在什么时候设置的呢?发现只有一处进行add,

1
2
3
4
5
6
7
8
9
10
11
12
//MergedDataBinderMapper.java
public void addMapper(DataBinderMapper mapper) {
Class<? extends DataBinderMapper> mapperClass = mapper.getClass();
//如果不在mExistingMappers中,才添加进mMappers
if (mExistingMappers.add(mapperClass)) {
mMappers.add(mapper);
final List<DataBinderMapper> dependencies = mapper.collectDependencies();
for(DataBinderMapper dependency : dependencies) {
addMapper(dependency);
}
}
}

再来看看谁调了addMapper,发现有一个生成类DataBinderMapperImpl(data binding通过apt创建了一些类),

1
2
3
4
5
6
7
8
//DataBinderMapperImpl.java
package androidx.databinding;//注意包名
public class DataBinderMapperImpl extends MergedDataBinderMapper {
DataBinderMapperImpl() {
//构造的时候把另一个包下的生成类DataBinderMapperImpl添加进去
addMapper(new com.holiday.jetpackstudy.DataBinderMapperImpl());
}
}

接着看业务包名下的生成类DataBinderMapperImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//DataBinderMapperImpl.java
package com.holiday.jetpackstudy;//注意包名
public class DataBinderMapperImpl extends DataBinderMapper {
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
switch(localizedLayoutId) {
case LAYOUT_ACTIVITYMAIN: {
if ("layout/activity_main_0".equals(tag)) {
//返回了binding的具体实现类
return new ActivityMainBindingImpl(component, view);
}
}
}
}
}

这里出现了tag,需要知道的是,DataBinding将布局文件拆成了两个文件,activity_main.xml描述布局,activity_main-layout.xml描述数据,activity_main.xmlapp/build/intermediates/incremental/mergeDebugResources/stripped.dir/layout/这个目录下,可见其被剔除了layout外壳和数据描述,同时根布局被加上了android:tag="layout/activity_main_0"

1
2
3
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:tag="layout/activity_main_0" >

activity_main-layout.xmlapp/build/intermediates/data_binding_layout_info_type_merge/debug/mergeDebugResources/out/目录下,里面可以看到TextView被设置了一个tag="binding_1"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" isMerge="false" layout="activity_main" modulePackage="com.holiday.jetpackstudy">
<Variables name="user" declared="true" type="User">
</Variables>
<Imports name="User" type="com.holiday.jetpackstudy.model.User">
</Imports>
<Targets>
<Target tag="layout/activity_main_0" view="ScrollView">
</Target>
<Target id="@+id/tv_name" tag="binding_1" view="TextView">
<Expressions>
<Expression attribute="android:text" text="user.name">
<TwoWay>false</TwoWay>
</Expression>
</Expressions>
</Target>
</Targets>
</Layout>

接下来跟进具体实现类ActivityMainBindingImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//ActivityMainBindingImpl.java
public ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent,View root) {
this(bindingComponent, root, mapBindings(bindingComponent, root, 2, sIncludes, sViewsWithIds));
//mapBindings会解析xml里data binding相关的tag,返回Object[]
//如:if (isRoot && tag != null && tag.startsWith("layout"))
//如:if (tag != null && tag.startsWith("binding_"))
}

private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
//bindings存储了布局文件里含tag的view,如bindings[0]是根布局,bindings[1]是TextView
//调用父类ActivityMainBinding的构造方法,为TextView赋值
super(bindingComponent, root, 0, (android.widget.TextView) bindings[1]);
this.mboundView0 = (android.widget.ScrollView) bindings[0];
//这里把tag置空,就不会影响到开发者自己写的tag
this.mboundView0.setTag(null);
this.tvName.setTag(null);
setRootTag(root);
invalidateAll();
}

//省略调用链:invalidateAll -> requestRebind -> mUIThreadHandler.post(mRebindRunnable);
// -> executePendingBindings -> executeBindingsInternal -> executeBindings

@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String userName = null;
com.holiday.jetpackstudy.model.User user = mUser;
if ((dirtyFlags & 0x3L) != 0) {
//这里对数据进行了判空,避免了空指针
if (user != null) {
userName = user.getName();
}
}
if ((dirtyFlags & 0x3L) != 0) {
//这里把数据设置给了TextView
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvName, userName);
}
}

最后补充一点,ActivityMainBinding这个类的位置在app/build/generated/data_binding_base_class_source_out/debug/dataBindingGenBaseClassesDebug/out/$业务包名/databinding/路径下,从这里可以找到binding能直接引用view的原因:

1
2
3
4
5
6
7
8
//ActivityMainBinding.java
public abstract class ActivityMainBinding extends ViewDataBinding {
public final TextView tvName;
protected ActivityMainBinding(Object _bindingComponent, View _root, int _localFieldCount,TextView tvName) {
super(_bindingComponent, _root, _localFieldCount);
this.tvName = tvName;
}
}

优缺点

  • 优点:
    • DataBinding会对绑定的数据进行判空,减少判空代码和空指针异常
    • 省去了找id操作,不会再出现id找不着的情况
  • 缺点:
    • apt创建了很多类,增大包体积和编译时长

参考文章