ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[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&lt;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&lt;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"/> ``` 注意:`<`必须转义书写为`&lt;` **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)