0%

Android-Jetpack组件之DataBinding

概述

DataBinding是一个绑定数据的支持库,使用该库,我们可以使用声明性格式而非编程方式将布局中的UI组件绑定到应用程序中的数据源。可在gradle中配置启用DataBinding:

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

基础使用

通过在layout根节点下添加data节点,可以使用@{expression}引用相关变量,其中expression表示表达式,但是尽量不要使用太复杂的表达式,会使得代码难以维护。布局文件如下:

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
44
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data class=".MainBinding">

<import
alias="MyUser"
type="com.hearing.mvvmdemo.bean.User" />

<variable
name="title"
type="String" />

<variable
name="user"
type="MyUser" />
</data>

<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<TextView
android:id="@+id/name_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{title, default=test}"
android:textSize="21sp" />

<TextView
android:id="@+id/age_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{String.valueOf(user.age)}"
android:textSize="21sp" />

</LinearLayout>
</layout>

假设上诉的xml文件名为activity_main.xml,Gradle会根据文件名生成一个ActivityMainBinding类,如上可以通过在data节点添加class属性更改默认的文件名。使用方式如下:

1
2
3
4
MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setLifecycleOwner(this);
binding.setUser(user);
binding.setTitle("title-title");

绑定可观察数据

Field

对于一些数据类,如果我们不想继承BaseObservable或者只需要其中几个字段支持可观察,那么可以使用这种方式来创建可观察数据:

1
2
ObservableField<String> name = new ObservableField<>();
ObservableInt age = new ObservableInt();

Collection

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"?>
<layout>
<data>
<import type="androidx.databinding.ObservableMap"/>
<import type="androidx.databinding.ObservableList"/>
<variable name="map" type="ObservableMap&lt;String, Object>"/>
<variable name="list" type="ObservableList&lt;Object>"/>
</data>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(map.count)}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(list[0])}" />
</layout>
  • List可以直接使用[index]运算符获取对应位置的元素
  • Map可以使用.运算符直接获取key对应的value,如果不想定义一个数据实体,可以直接使用Map来替代

Object

使用@Bindable注解的属性会在BR类中生成一条记录。

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
public class User extends BaseObservable {

private String name;
private int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

@Bindable
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
// notifyChange();
}

@Bindable
public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
notifyPropertyChanged(BR.age);
}
}

LiveData

LiveData是首选的一种方式,用法:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data class=".MainBinding">
<variable
name="title"
type="androidx.lifecycle.LiveData&lt;String>" />
</data>

<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<TextView
android:id="@+id/name_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{title}"
android:textSize="21sp" />
</LinearLayout>
</layout>

使用时:

1
2
3
4
5
6
7
8
9
10
11
final MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
final MutableLiveData<String> title = new MutableLiveData<>("title-title");
binding.setLifecycleOwner(this); // 需要设置LifecycleOwner
binding.setTitle(title);
new Thread(new Runnable() {
@Override
public void run() {
sleep(2000);
title.postValue("title");
}
}).start();

双向绑定

上述的单向绑定是指数据变化后更新UI,而双向绑定是指其中任意一个变化后都会同步更新到另一个。双向绑定使用@={}表达式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data class=".MainBinding">
<variable
name="text"
type="androidx.databinding.ObservableField&lt;String>" />
</data>

<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={text}" />
</LinearLayout>
</layout>

使用如下:

1
2
3
4
5
6
7
8
9
final MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
ObservableField<String> text = new ObservableField<>("text");
binding.setText(text);
text.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
Log.d(TAG, "s = " + binding.getText().get());
}
});

事件绑定

定义事件处理方法:

1
2
3
4
5
6
7
8
9
10
11
public class EventHandler {
private static final String TAG = "jetPack";

public void onClick(View view) {
Log.d(TAG, "onClick");
}

public void onHandler() {
Log.d(TAG, "onHandler");
}
}

布局文件中引用:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data class=".MainBinding">

<variable
name="handler"
type="com.hearing.mvvmdemo.bean.EventHandler" />
</data>

<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<TextView
android:id="@+id/name_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handler::onClick}"
android:padding="10dp"
android:text="@string/app_name"
android:textSize="21sp" />

<TextView
android:id="@+id/age_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->handler.onHandler()}"
android:padding="10dp"
android:text="@string/app_name"
android:textSize="21sp" />

</LinearLayout>
</layout>

在Java中使用:

1
2
3
final MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setLifecycleOwner(this);
binding.setHandler(new EventHandler());

BindingAdapter

Android中已经支持了许多可绑定的参数,如android:textandroid:onClick等,也可以使用BindingAdapter来自定义参数,定义一个静态方法:

1
2
3
4
@BindingAdapter("imageUrl")
public static void setUrl(ImageView view, String url) {
Glide.with(view).load(url).into(view);
}

注意第一个参数需要为View,在布局文件中可以使用这个自定义参数:

1
2
3
4
5
<ImageView
imageUrl="@{url}"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center_horizontal" />

在代码中可以这样设置:

1
2
final MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setUrl(url);

如果使用的是Kotlin,可以直接将其定义为扩展函数:

1
2
@BindingAdapter("imageUrl")
fun ImageView.loadImage(url: String) = Glide.with(this.context).load(url).into(this)

还可以配置多参数的BindingAdapter:

1
2
3
4
@BindingAdapter(value = {"age", "title"}, requireAll = true)
public static void setText(TextView view, int age, String title) {
view.setText(title + ": " + age);
}

对应的布局文件:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data class=".MainBinding">
<variable name="age" type="int" />
<variable name="title" type="String" />
</data>

<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<TextView
android:id="@+id/name_tv"
age="@{age}"
title="@{title}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:textSize="21sp" />

</LinearLayout>
</layout>

DataBinding工作流程

先跳过编译期间生成代码的逻辑,直接看生成的代码之间是怎么配合工作的。首先给出一个layout文件:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data class=".MainBinding">
<variable name="title" type="String" />
</data>

<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<TextView
android:id="@+id/name_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{title}"
android:textSize="21sp" />

</LinearLayout>
</layout>

接下来贴出生成的部分文件:

activity_main-layout.xml

该文件包含了布局文件的一些Binding信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout bindingClass=".MainBinding" directory="layout" filePath="D:\Workspace\Android\MvvmDemo\app\src\main\res\layout\activity_main.xml"
isBindingData="true" isMerge="false" layout="activity_main" modulePackage="com.hearing.mvvmdemo" rootNodeType="android.widget.LinearLayout">
<ClassNameLocation endLine="4" endOffset="28" startLine="4" startOffset="17" />
<Variables name="title" declared="true" type="String">
<location endLine="8" endOffset="27" startLine="6" startOffset="8" />
</Variables>
<Targets>
<Target id="@+id/container" tag="layout/activity_main_0" view="LinearLayout">
<Expressions />
<location endLine="26" endOffset="18" startLine="11" startOffset="4" />
</Target>
<Target id="@+id/name_tv" tag="binding_1" view="TextView">
<Expressions>
<Expression attribute="android:text" text="title">
<Location endLine="23" endOffset="34" startLine="23" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="23" endOffset="32" startLine="23" startOffset="28" />
</Expression>
</Expressions>
<location endLine="24" endOffset="37" startLine="18" startOffset="8" />
</Target>
</Targets>
</Layout>

activity_main.xml

该文件是编译器移除DataBinding相关的标签后生成的标准的layout文件,主要就是多了一个tag标签,用来在后面的代码里检索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="1"
android:id="@ref/0x7f070049"
android:tag="layout/activity_main_0"
android:layout_width="-1"
android:layout_height="-1">

<TextView
android:textSize="dimension(5378)"
android:id="@ref/0x7f07006e"
android:tag="binding_1"
android:padding="dimension(2561)"
android:layout_width="-1"
android:layout_height="-2" />
</LinearLayout>

BR

该类记录layout中所有的变量。

1
2
3
4
5
public class BR {
public static final int _all = 0;

public static final int title = 1;
}

MainBinding

该类是一个abstract类,继承自ViewDataBinding类,它的实现类也是一个自动生成的类:MainBindingImpl。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public abstract class ViewDataBinding extends BaseObservable implements ViewBinding {
// ...
}

public abstract class MainBinding extends ViewDataBinding {
@NonNull
public final LinearLayout container;

@NonNull
public final TextView nameTv;

@Bindable
protected String mTitle;

protected MainBinding(Object _bindingComponent, View _root, int _localFieldCount,
LinearLayout container, TextView nameTv) {
super(_bindingComponent, _root, _localFieldCount);
this.container = container;
this.nameTv = nameTv;
}

public abstract void setTitle(@Nullable String title);

@Nullable
public String getTitle() {
return mTitle;
}

@NonNull
public static MainBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup root,
boolean attachToRoot) {
return inflate(inflater, root, attachToRoot, DataBindingUtil.getDefaultComponent());
}

@NonNull
@Deprecated
public static MainBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup root,
boolean attachToRoot, @Nullable Object component) {
return ViewDataBinding.<MainBinding>inflateInternal(inflater, R.layout.activity_main, root, attachToRoot, component);
}

@NonNull
public static MainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, DataBindingUtil.getDefaultComponent());
}

@NonNull
@Deprecated
public static MainBinding inflate(@NonNull LayoutInflater inflater, @Nullable Object component) {
return ViewDataBinding.<MainBinding>inflateInternal(inflater, R.layout.activity_main, null, false, component);
}

public static MainBinding bind(@NonNull View view) {
return bind(view, DataBindingUtil.getDefaultComponent());
}

@Deprecated
public static MainBinding bind(@NonNull View view, @Nullable Object component) {
return (MainBinding) bind(component, view, R.layout.activity_main);
}
}

MainBindingImpl

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public class MainBindingImpl extends MainBinding {

@Nullable
private static final androidx.databinding.ViewDataBinding.IncludedLayouts sIncludes;
@Nullable
private static final android.util.SparseIntArray sViewsWithIds;

static {
sIncludes = null;
sViewsWithIds = null;
}
// views
// variables
// values
// listeners
// Inverse Binding Event Handlers

public MainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
this(bindingComponent, root, mapBindings(bindingComponent, root, 2, sIncludes, sViewsWithIds));
}

private MainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 0
, (android.widget.LinearLayout) bindings[0]
, (android.widget.TextView) bindings[1]
);
this.container.setTag(null);
this.nameTv.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}

@Override
public void invalidateAll() {
synchronized (this) {
mDirtyFlags = 0x2L;
}
requestRebind();
}

@Override
public boolean hasPendingBindings() {
synchronized (this) {
if (mDirtyFlags != 0) {
return true;
}
}
return false;
}

@Override
public boolean setVariable(int variableId, @Nullable Object variable) {
boolean variableSet = true;
if (BR.title == variableId) {
setTitle((java.lang.String) variable);
} else {
variableSet = false;
}
return variableSet;
}

public void setTitle(@Nullable java.lang.String Title) {
this.mTitle = Title;
synchronized (this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.title);
super.requestRebind();
}

@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
}
return false;
}

@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized (this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String title = mTitle;

if ((dirtyFlags & 0x3L) != 0) {
}
// batch finished
if ((dirtyFlags & 0x3L) != 0) {
// api target 1

androidx.databinding.adapters.TextViewBindingAdapter.setText(this.nameTv, title);
}
}

// Listener Stub Implementations
// callback impls
// dirty flag
private long mDirtyFlags = 0xffffffffffffffffL;
/* flag mapping
flag 0 (0x1L): title
flag 1 (0x2L): null
flag mapping end*/
//end
}

setContentView

接下来看看这个方法是怎么实现的:

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
final MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

// DataBindingUtil
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId, @Nullable DataBindingComponent bindingComponent) {
// 调用当前Activity的setContentView方法
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}

// parent:当前Activity的contentView
// layoutId:布局文件的id
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
ViewGroup parent, int startChildren, int layoutId) {
final int endChildren = parent.getChildCount();
// 获取当前contentView的子view数
final int childrenAdded = endChildren - startChildren;
if (childrenAdded == 1) {
final View childView = parent.getChildAt(endChildren - 1);
return bind(component, childView, layoutId);
} else {
final View[] children = new View[childrenAdded];
for (int i = 0; i < childrenAdded; i++) {
children[i] = parent.getChildAt(i + startChildren);
}
return bind(component, children, layoutId);
}
}

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

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

private static DataBinderMapper sMapper = new DataBinderMapperImpl();

这里的sMapper是DataBinderMapperImpl实例,其实现如下:

1
2
3
4
5
public class DataBinderMapperImpl extends MergedDataBinderMapper {
DataBinderMapperImpl() {
addMapper(new com.hearing.mvvmdemo.DataBinderMapperImpl());
}
}

可以看出sMapper.getDataBinder方法的实现在MergedDataBinderMapper类中(只看参数为View的,参数为View[]的类似):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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;
}
}
if (loadFeatures()) {
return getDataBinder(bindingComponent, view, layoutId);
}
return null;
}

这里的mMappers中的元素即是上面DataBinderMapperImpl中addMapper添加的com.hearing.mvvmdemo.DataBinderMapperImpl类(这俩类名字一样但是不同包)。因此看看com.hearing.mvvmdemo.DataBinderMapperImpl中的getDataBinder方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if (localizedLayoutId > 0) {
final Object tag = view.getTag();
if (tag == null) {
throw new RuntimeException("view must have a tag");
}
switch (localizedLayoutId) {
case LAYOUT_ACTIVITYMAIN: {
// 这里的tag可以在上面的layout文件中看到,因此这里返回的MainBindingImpl即是对应activity_main布局文件
if ("layout/activity_main_0".equals(tag)) {
return new MainBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
}
}
}
return null;
}

回到最开始的final MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main),这里返回的binding即是MainBindingImpl类的实例。

setTitle

接下来看看这段代码的实现:

1
2
3
4
5
6
7
8
9
10
11
binding.setTitle("title");

// MainBindingImpl
public void setTitle(@Nullable java.lang.String Title) {
this.mTitle = Title;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.title);
super.requestRebind();
}

setTitle方法首先给mTitle赋值,然后调用notifyPropertyChanged方法,requestRebind方法最后会调用到MainBindingImpl#executeBindings方法给目标TextView设置text:

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
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String title = mTitle;

if ((dirtyFlags & 0x3L) != 0) {
}
if ((dirtyFlags & 0x3L) != 0) {
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.nameTv, title);
}
}

// TextViewBindingAdapter
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}

requestRebind方法最后会重新给TextView赋值。当 Binding 中使用的数据是 LiveData 时,在调用 binding.setXXX 方法内部,会给 LiveData 添加观察者监听,所以才可对 LiveData 数据变化做出反应。其它可观测的数据类型也类似。

DataBinding生成代码

关于这部分的源码可以在data-binding下载。入口可以从这个类开始:android.databinding.annotationprocessor.ProcessDataBinding,这是一个注解处理器。

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
44
45
46
47
48
49
50
51
52
@SupportedAnnotationTypes({
"android.databinding.BindingAdapter",
"android.databinding.Untaggable",
"android.databinding.BindingMethods",
"android.databinding.BindingConversion",
"android.databinding.BindingBuildInfo"}
)
public class ProcessDataBinding extends AbstractProcessor {
private List<ProcessingStep> mProcessingSteps;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (mProcessingSteps == null) {
initProcessingSteps();
}
final BindingBuildInfo buildInfo = BuildInfoUtil.load(roundEnv);
if (buildInfo == null) {
return false;
}
boolean done = true;
for (ProcessingStep step : mProcessingSteps) {
try {
done = step.runStep(roundEnv, processingEnv, buildInfo) && done;
} catch (JAXBException e) {
L.e(e, "Exception while handling step %s", step);
}
}
if (roundEnv.processingOver()) {
for (ProcessingStep step : mProcessingSteps) {
step.onProcessingOver(roundEnv, processingEnv, buildInfo);
}
}
Scope.assertNoError();
return done;
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}

private void initProcessingSteps() {
final ProcessBindable processBindable = new ProcessBindable();
mProcessingSteps = Arrays.asList(
new ProcessMethodAdapters(),
new ProcessExpressions(),
processBindable
);
// ...
}

// ...
}

上面主要处理三个ProcessingStep的子类:

  • ProcessMethodAdapters:检查哪些类或者方法有这些注解:@BindingAdapter, @Untaggable, @BindingMethods, @BindingConversion, @InverseBindingAdapter, @InverseBindingMethods,然后把它们保存在SetterStore中。在编译期间,注解处理器拿到的这些信息会被存放在pkgxxx-setter_store.bin文件中。
  • ProcessExpressions:这个类用来处理表达式,在这一步中会搜索工程中所有xml文件并且会转换最外层为<layout></layout>标签支持data-binding的xml文件,它把这个文件拆分为2个文件:activity_main.xml(正常的布局文件)和activity_main-layout.xml(包含绑定信息)。
  • ProcessBindable:生成BR类,绑定属性的id。

在上面的步骤中,DataBinderWriter和LayoutBinderWriter等类会生成我们需要的MainBinding等类。这部分代码比较多,所以只是大概过了一下。

ViewBinding

概述

在Android Studio 3.6 Canary 11及更高版本中可以使用ViewBinding,这是Google提供的一个避免使用过多findViewById的库。之前可以使用Jake Wharton大神的Butter Knife开源库,最近Butter Knife的readme上有一句话:Attention: This tool is now deprecated. Please switch to view binding,即推荐使用ViewBinding。

配置

ViewBinding可以按模块启用,在要启用的module gradle中配置:

1
2
3
4
5
6
android {
// ...
viewBinding {
enabled = true
}
}

如果希望在生成绑定类时忽略某个布局文件,可以添加:

1
2
3
<LinearLayout tools:viewBindingIgnore="true" >
<!-- ... -->
</LinearLayout>

假设layout文件如下:

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"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<TextView
android:id="@+id/name_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{title}"
android:textSize="21sp" />

</LinearLayout>

ActivityMainBinding

ActivityMainBinding是上述layout生成的类,内容如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;

@NonNull
public final LinearLayout container;

@NonNull
public final TextView nameTv;

private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull LinearLayout container,
@NonNull TextView nameTv) {
this.rootView = rootView;
this.container = container;
this.nameTv = nameTv;
}

@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}

@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}

@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}

@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
String missingId;
missingId:
{
LinearLayout container = rootView.findViewById(R.id.container);
if (container == null) {
missingId = "container";
break missingId;
}
TextView nameTv = rootView.findViewById(R.id.name_tv);
if (nameTv == null) {
missingId = "nameTv";
break missingId;
}
return new ActivityMainBinding((LinearLayout) rootView, container, nameTv);
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}

其中ViewBinding是一个接口:

1
2
3
4
5
6
7
8
package androidx.viewbinding;

/** A type which binds the views in a layout XML to fields. */
public interface ViewBinding {
// 返回根视图
@NonNull
View getRoot();
}

使用

在Activity中:

1
2
3
4
5
6
7
8
9
10
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.nameTv.setText("hearing");
}
}

在Fragment中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainFragment extends Fragment {
private ActivityMainBinding mBinding;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = ActivityMainBinding.inflate(inflater, container, false);
mBinding.nameTv.setText("hearing");
return mBinding.getRoot();
}

@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}

总结

与findViewById的区别:

  • Null安全:由于ViewBinding会创建对View的直接引用,因此不存在因View ID无效而引发Null指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用@Nullable标记。
  • 类型安全:每个绑定类中的字段均具有与它们在XML文件中引用的View相匹配的类型,这意味着不存在发生类转换异常的风险。

这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。

与DataBinding的对比,二者均会生成可用于直接引用视图的绑定类。但是,ViewBinding旨在处理更简单的用例,与DataBinding相比,具有以下优势:

  • 更快的编译速度:不需要处理注解,因此编译时间更短。
  • 易于使用:不需要特别标记的XML布局文件,因此在应用中使用速度更快。在模块中启用ViewBinding后,它会自动应用于该模块的所有布局。

反过来,与DataBinding相比,ViewBinding也具有以下限制:

  • ViewBinding不支持布局变量或布局表达式,因此不能用于直接在XML布局文件中声明动态界面内容。
  • ViewBinding不支持双向数据绑定。

考虑到这些因素,在某些情况下,最好在项目中同时使用ViewBinding和DataBinding。可以在需要高级功能的布局中使用DataBinding,而在不需要高级功能的布局中使用ViewBinding。

总结

DataBinding经常结合ViewModel&LivaData一起使用,DataBinding的作用:

  1. 取代烦琐的findviewById,自动在binding中生成对应的view;
  2. 绑定VM层和V层的监听关系,使得VM和V层的交互更简单。一是VM通知V层作改变,比如setText等;二是V层通知VM作改变,比如click事件,edit的输入等。

DataBinding工作流程(双向绑定和观察者模式):

  1. DataBindingUtil.setContentView方法将xml中的各个View赋值给ViewDataBinding,完成findviewById的任务;
  2. ViewDataBinding的setVariable等方法建立了ViewDataBinding与VM之间的联系,也就搭建了一个可以互相通信的桥梁;
  3. 当VM层调用notifyPropertyChanged方法时,最终在ViewDataBinding实现类的executeBindings方法中处理逻辑。

ViewBinding和DataBinding代码生成:

  • DataBinding:通过注解处理器和Android Gradle插件,在编译期间生成需要的代码。
  • ViewBinding:通过Android Gradle插件,在编译期扫描开启了ViewBinding的模块下的layout文件,生成对应的binding类。