[TOC]
# 生成绑定类
引入数据绑定库后,系统会为每个布局文件生成一个绑定类,绑定类会将布局变量与布局中的视图关联起来。创建绑定对象时,会为布局变量自动生成setter和getter方法。
```xml
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
// 布局变量
<data
class="CustomBinding">
<import type="java.util.List" />
<variable
name="stringList"
type="List<String>" />
</data>
// 布局视图
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
//...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```
生成绑定类共有以下几种方式,以Activity为示例:
**方式1:**
```java
CustomBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
```
**方式2:**
```java
CustomBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.activity_main, null, false);
setContentView(binding.getRoot());
```
**方式3:**
```java
View view = getLayoutInflater().inflate(R.layout.activity_main, null, false);
CustomBinding binding = DataBindingUtil.bind(view);
setContentView(binding.getRoot());
```
**方式4:**
此方法最终调用的是DataBindingUtil的bind方法。
```java
View view = getLayoutInflater().inflate(R.layout.activity_main, null, false);
CustomBinding binding = CustomBinding.bind(view);
setContentView(binding.getRoot());
```
**方式5:**
此方法最终调用的是DataBindingUtil的inflate方法。且此方法已经Deprecated。
```java
CustomBinding binding = CustomBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
```
以上5种方法中,方法1-3调用DataBindingUtil的方法创建绑定对象;方法4-5调用绑定类的方法创建绑定对象,方法4-5中绑定类的方法最终调用的也是DataBindingUtil的方法。所以推荐直接调用DataBindingUtil的方法。
## 源码分析
下面重点关注方法1和方法2,看看它们的源码。
**1、DataBindingUtil的setContentView方法**
方法1DataBindingUtil的setContentView方法,仅适用于Activity的绑定,源码如下:
```java
// DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity, int layoutId) {
return setContentView(activity, layoutId, sDefaultComponent);
}
// DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId, @Nullable DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
```
DataBindingUtil的setContentView方法会自动为Activity设置ContentView。
重点关注下bindToAddedViews方法,源码如下:
```java
// DataBindingUtil.java
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
ViewGroup parent, int startChildren, int layoutId) {
final int endChildren = parent.getChildCount();
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);
}
}
```
bindToAddedViews的第二个参数是parent,第三个参数是startChildren,需要注意,bindToAddedViews方法绑定的是parent父布局内部的子View或子View集合。对于Activity来说,parent就是contentView,子View也就是我们Activity布局文件的根布局。
**2、DataBindingUtil的inflate方法**
方法2DataBindingUtil的inflate方法适用于Activity、Fragment、Adapter等。
```java
// DataBindingUtil.java
public static <T extends ViewDataBinding> T inflate(@NonNull LayoutInflater inflater,
int layoutId, @Nullable ViewGroup parent, boolean attachToParent) {
return inflate(inflater, layoutId, parent, attachToParent, sDefaultComponent);
}
// DataBindingUtil.java
public static <T extends ViewDataBinding> T inflate(
@NonNull LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent,
boolean attachToParent, @Nullable DataBindingComponent bindingComponent) {
final boolean useChildren = parent != null && attachToParent;
// 1、startChildren是目标加载View在parent中的index
final int startChildren = useChildren ? parent.getChildCount() : 0;
// root不空且附加到root时,返回的是root(当然View已经附加到里面了);否则返回的是目标加载View
final View view = inflater.inflate(layoutId, parent, attachToParent);
if (useChildren) {
// 1.1 绑定的是parent父布局内部的子View或子View集合
return bindToAddedViews(bindingComponent, parent, startChildren, layoutId);
} else {
// 2、直接绑定当前布局文件加载出来的View视图
return bind(bindingComponent, view, layoutId);
}
}
```
1、当parent不为null,且附加到parent时,调用bindToAddedViews方法,传递过去的参数是parent和目标View的index。最终绑定parent父布局的子View,也就是我们的目标加载View
2、当parent为null,或不添加到parent时,直接调用bind方法,绑定当前布局文件加载出来的View视图。
两种情况的区别在于,绑定出的View是否具有LayoutParams。
# 绑定表达式
## 表达式语言
### 引用
**1、对象属性引用**
表达式可以使用以下格式在类中引用对象的属性,对于字段、getter 和[`ObservableField`](https://developer.android.com/reference/androidx/databinding/ObservableField)对象都一样。
```xml
<data>
<variable
name="user"
type="com.markxu.notitest.User" />
</data>
android:text="@{user.lastName}"
```
**2、集合引用**
```xml
<data>
<import type="java.util.List" />
<variable
name="stringList"
type="List<String>" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{stringList.get(0)}'
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
```
注意:`<`必须转义书写为`<`
**3、资源引用**
```xml
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
```
### 避免 Null 指针异常
生成的数据绑定代码会自动检查有没有`null`值并避免出现 Null 指针异常。例如,在表达式`@{user.name}`中,如果`user`为 Null,则为`user.name`分配默认值`null`。
### Null合并运算符
如果左边运算数不是`null`,则 Null 合并运算符 (`??`) 选择左边运算数,如果左边运算数为`null` ,则选择右边运算数。
```xml
android:text="@{user.displayName ?? user.lastName}"
等价于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
```
## 事件处理
通过数据绑定,可以编写从视图分派的表达式处理事件。有方法引用和监听器绑定两种机制。
使用方法引用方式进行事件处理时,在编译时就会实现监听器。如果方法不存在或签名不正确,会收到编译错误;使用监听器绑定方式进行事件处理时,事件发生时才会实现监听器。
首先熟悉[Lambda表达式的使用](http://www.androidwiki.site/1569131#Lambda_1),理解事件处理会更容易些。
### 方法引用
当表达式求值结果为方法引用时,数据绑定会将方法引用和所有者对象封装到监听器中,并在目标视图上设置该监听器。
```java
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
```
```xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers"/>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>
```
**备注**
使用Lambda表达式时,当已经有现成的方法可以完成你想要传递到其他代码的某个动作时,可以直接使用方法引用。
表达式`handlers::onClickFriend`就是一个方法引用,等价于lambda表达式`(v) -> handlers.onClickFriend(v)`,具体可参考[http://www.androidwiki.site/1569131#\_48](http://www.androidwiki.site/1569131#_48)
### 监听器绑定
事件发生时对lambda表达式进行求值。
```java
public class Presenter {
public void onSaveClick(Task task){}
}
```
```xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>
```
监听器绑定中,可以忽略方法的所有参数`android:onClick="@{() -> presenter.onSaveClick(task)}"`,也可以命名所有参数`android:onClick="@{(view) -> presenter.onSaveClick(task)}"`
**备注**
使用Lambda表达式时,不能忽略方法的参数
# 使用可观察数据对象
任何 plain-old 对象都可用于数据绑定,但修改对象不会自动使界面更新。通过数据绑定,数据对象可在其数据发生更改时通知其他对象,自动更新界面。
## 可观察对象
将我们的对象继承BaseObservable即可实现可观察功能。
```java
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
```
向getter分配`Bindable`,在setter中调用`notifyPropertyChanged()`方法即可。
## 可观察字段
除了使用继承BaseObservable的方式实现可观察对象,还可以直接使用可观察字段。
```java
private static class User {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
```
Android系统提供了如下可观察字段:
* [`ObservableBoolean`](https://developer.android.com/reference/androidx/databinding/ObservableBoolean)
* [`ObservableByte`](https://developer.android.com/reference/androidx/databinding/ObservableByte)
* [`ObservableChar`](https://developer.android.com/reference/androidx/databinding/ObservableChar)
* [`ObservableShort`](https://developer.android.com/reference/androidx/databinding/ObservableShort)
* [`ObservableInt`](https://developer.android.com/reference/androidx/databinding/ObservableInt)
* [`ObservableLong`](https://developer.android.com/reference/androidx/databinding/ObservableLong)
* [`ObservableFloat`](https://developer.android.com/reference/androidx/databinding/ObservableFloat)
* [`ObservableDouble`](https://developer.android.com/reference/androidx/databinding/ObservableDouble)
* [`ObservableParcelable`](https://developer.android.com/reference/androidx/databinding/ObservableParcelable)
* [`ObservableArrayMap`](https://developer.android.com/reference/androidx/databinding/ObservableArrayMap)
* [`ObservableArrayList`](https://developer.android.com/reference/androidx/databinding/ObservableArrayList)
# 绑定适配器
## 设置绑定值
绑定适配器的作用是,View的属性绑定的值发生变化时,告诉数据绑定库应该调用哪个方法来更新View。
**注意:**
View的属性在初次绑定时,以及绑定值发生变化时,数据绑定库都会调用View的方法更新UI。
```java
TextView tvName = findViewById(R.id.tv_name);
tvName.setText("Mark");
```
不使用数据绑定库时,更新UI控件需要首先通过findViewById方法找到控件,再调用控件的setter方法改变属性更新UI。如上面例子中,先通过findViewById找到TextView控件,再调用TextView的setText方法,来给TextView的text属性赋值,以更新TextView所展示的文案。
```xml
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
android:textSize="24sp" />
```
使用数据绑定库后,只要绑定值`user.name`发生更改,绑定类就会在视图上调用setter方法(相当于自动调用TextView的setText方法),来更新UI,前面的赋值更新UI操作全部由数据绑定库帮我们完成了。
绑定值发生变化时,数据绑定库调用哪个方法来更新控件呢,有如下三种情况:
**1、让数据绑定库自己决定调用哪个方法**
还以`android:text="@{user.name}"`为例,绑定值发生变更时,数据绑定库会自动在TextView类中查找`setText`方法,方法参数需要与`user.name`的值一致。在TextView类中找到`setText(CharSequence text)`方法后,自动调用。
**2、指定自定义方法名称**
View的某些特性拥有名称不符的 setter时,可以指定想让数据绑定库调用的方法。
```java
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
```
上面示例中,`android:tint`属性绑定的值变化时,数据绑定库会去控件类中寻找`setImageTintList(ImageView imageView)`方法并调用,而不是去寻找`setTint()`方法。
**3、提供自定义逻辑**
某些情况下,没有现成的方法供数据绑定库去调用,我们可以提供自定义逻辑。
例如,TextView的`android:paddingLeft`特性没有关联的 setter,也就是TextView没有setPaddingLeft方法,但是TextView提供了`setPadding(left, top, right, bottom)`方法。此时可以使用BindingAdapter来创建绑定适配器:
```java
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int paddingLeft) {
view.setPadding(paddingLeft,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
```
参数类型非常重要。第一个参数确定与特性关联的视图类型(TextView),第二个参数确定View属性绑定表达式中接受的类型(`android:paddingLeft`对应的类型)。
即,TextView的`android:paddingLeft`绑定的值发生变更时,数据绑定库根据BindingAdapter注解找到setPaddingLeft方法并调用,会把待更新的View和变更后的值传递过去。
**接收多个特性的适配器:**
```java
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.get().load(url).error(error).into(view);
}
```
```xml
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}" />
```
如果ImageView对象同时设置了`imageUrl`和`error`,并且`imageUrl`是字符串,`error`是Drawable,则会调用适配器。如果希望在设置了任意特性时调用适配器,则可以将适配器的`requireAll`设置为`false`,如下示例所示:
```java
@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
```
> 备注:
数据绑定库在匹配时会忽略自定义命名空间。
## 对象转换
**1、自动对象转换**
```xml
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
```
View属性绑定值返回的为Object时,数据绑定库会自动进行对象转换,将Object对象转换为text属性对应的setText方法所需要的参数类型,也就是String类型。
**2、自定义转换**
```xml
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
```
某些情况下,View属性绑定值返回的类型可以自定义转换。上面例子中,background属性需要的是Drawable类型的值,但color是整型值,此时可以使用`BindingConversion`注解完成转换:
```java
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
```
绑定表达式中提供的值类型必须保持一致。您不能在同一个表达式中使用不同的类型,如以下示例所示:
```xml
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
```
自定义转换的应用场景:假如某个控件需要一个格式化好的时间,但是目前只有一个`Date`类型额变量,除了转化完成之后再进行设置时间,现在还可以使用自定义转换的方式,先设置再转换。
# 总结
在没有使用数据绑定库的时候,更新UI需要先找到控件,再调用控件的方法更改控件属性来更新UI。引入数据绑定库后,数据绑定库直接将控件的属性值和绑定表达式的值(数据源)进行绑定,当绑定表达式值变更时,数据绑定库会自动去寻找控件对象的合适的方法,自动调用,来更新UI。
因此本文的主要内容也就是:怎么将视图和数据源(绑定表达式)进行绑定,绑定表达式的规则,赋予数据源变化时数据绑定库自动调用控件方法的能力,帮助数据绑定库选择应该调用哪个方法,数据源返回对象的转换。
# 参考文档
[官方文档:绑定适配器](https://developer.android.com/topic/libraries/data-binding/binding-adapters#object-conversions)
[Android官方数据绑定框架DataBinding(二)](https://blog.csdn.net/qibin0506/article/details/47720125)
- 导读
- Java知识
- Java基本程序设计结构
- 【基础知识】Java基础
- 【源码分析】Okio
- 【源码分析】深入理解i++和++i
- 【专题分析】JVM与GC
- 【面试清单】Java基本程序设计结构
- 对象与类
- 【基础知识】对象与类
- 【专题分析】Java类加载过程
- 【面试清单】对象与类
- 泛型
- 【基础知识】泛型
- 【面试清单】泛型
- 集合
- 【基础知识】集合
- 【源码分析】SparseArray
- 【面试清单】集合
- 多线程
- 【基础知识】多线程
- 【源码分析】ThreadPoolExecutor源码分析
- 【专题分析】volatile关键字
- 【面试清单】多线程
- Java新特性
- 【专题分析】Lambda表达式
- 【专题分析】注解
- 【面试清单】Java新特性
- Effective Java笔记
- Android知识
- Activity
- 【基础知识】Activity
- 【专题分析】运行时权限
- 【专题分析】使用Intent打开三方应用
- 【源码分析】Activity的工作过程
- 【面试清单】Activity
- 架构组件
- 【专题分析】MVC、MVP与MVVM
- 【专题分析】数据绑定
- 【面试清单】架构组件
- 界面
- 【专题分析】自定义View
- 【专题分析】ImageView的ScaleType属性
- 【专题分析】ConstraintLayout 使用
- 【专题分析】搞懂点九图
- 【专题分析】Adapter
- 【源码分析】LayoutInflater
- 【源码分析】ViewStub
- 【源码分析】View三大流程
- 【源码分析】触摸事件分发机制
- 【源码分析】按键事件分发机制
- 【源码分析】Android窗口机制
- 【面试清单】界面
- 动画和过渡
- 【基础知识】动画和过渡
- 【面试清单】动画和过渡
- 图片和图形
- 【专题分析】图片加载
- 【面试清单】图片和图形
- 后台任务
- 应用数据和文件
- 基于网络的内容
- 多线程与多进程
- 【基础知识】多线程与多进程
- 【源码分析】Handler
- 【源码分析】AsyncTask
- 【专题分析】Service
- 【源码分析】Parcelable
- 【专题分析】Binder
- 【源码分析】Messenger
- 【面试清单】多线程与多进程
- 应用优化
- 【专题分析】布局优化
- 【专题分析】绘制优化
- 【专题分析】内存优化
- 【专题分析】启动优化
- 【专题分析】电池优化
- 【专题分析】包大小优化
- 【面试清单】应用优化
- Android新特性
- 【专题分析】状态栏、ActionBar和导航栏
- 【专题分析】应用图标、通知栏适配
- 【专题分析】Android新版本重要变更
- 【专题分析】唯一标识符的最佳做法
- 开源库源码分析
- 【源码分析】BaseRecyclerViewAdapterHelper
- 【源码分析】ButterKnife
- 【源码分析】Dagger2
- 【源码分析】EventBus3(一)
- 【源码分析】EventBus3(二)
- 【源码分析】Glide
- 【源码分析】OkHttp
- 【源码分析】Retrofit
- 其他知识
- Flutter
- 原生开发与跨平台开发
- 整体归纳
- 状态及状态管理
- 零碎知识点
- 添加Flutter到现有应用
- Git知识
- Git命令
- .gitignore文件
- 设计模式
- 创建型模式
- 结构型模式
- 行为型模式
- RxJava
- 基础
- Linux知识
- 环境变量
- Linux命令
- ADB命令
- 算法
- 常见数据结构及实现
- 数组
- 排序算法
- 链表
- 二叉树
- 栈和队列
- 算法时间复杂度
- 常见算法思想
- 其他技术
- 正则表达式
- 编码格式
- HTTP与HTTPS
- 【面试清单】其他知识
- 开发归纳
- Android零碎问题
- 其他零碎问题
- 开发思路