ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 背景 略。 # 环境设置 要开始使用databinding,需要在module里面的build.gradle配置好相关的环境: 具体参照以下代码: android { .... dataBinding { enabled = true } } 如果一个library module设置了databinding,那么app module也*必须* 在build.gradle加上以上代码。 # 在布局文件中使用databinding ## 示例代码 数据绑定布局文件和一般布局文件略有不同,它以`<layout>`的根标签开始,后跟`<data>`标签和*视图根元素*,即一般的视图布局。示例文件如下所示: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <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}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}"/> </LinearLayout> </layout> 其中`<data>`标签里面的`<variable>`标签声明好需要绑定的数据,后面还可以看到`<import>`等标签的使用。 ## 绑定语法 * 数据绑定 绑定数据的表达式通过 `@{}` 语法写入标签的属性里面,例如:`android:text="@{user.firstName}"`这句代码就是把user.firstName设置到`android:text`属性里面。 `@{}`里面的表达式,如果是对象引用,例如`user.firstName`,表达式求值的时候,会首先查找对象的getter方法,即`getFirstName()`方法。如果存在此方法,则以返回值作为表达式的值。如果不存在对应的getter,则会查找公开的字段,如果没有相同命名的公开字段,编译时就会报错。 双向绑定的语法是 `@={}` ,需要注意的是这是一项实验性功能,尚未到正式版本。 * 事件处理 事件绑定语法和数据绑定差不多,都是通过 `@{}` 把方法的引用([Java8的语法](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html))给到特定的属性。 但相对于属性,设置方法的语法还是需要留意一下: * 基本语法:把一个lambda表达式或者通过方法引用语法`::`设置方法到事件属性里面,例如最常见的点击事件 android:onClick="@{(v) -> presenter.onClick()}" 或者是: android:onClick="@{presenter::onClickView}" 使用`::`语法的时候,需要保证的是`pesenter.onClickView`方法拥有和对应的事件方法同样的参数,即`onClickView`方法的定义必须是下面这样: public T onClickView(View view){ ... } * 假如需要方法里面的参数,只要需要其中一个,都要把所有参数写上: app:onTagClickListener="@{(view,tag,position) -> presenter.addListItem(position)}" * 假如不需要任何方法里面的参数,那么括号里面的参数列表可以省略: app:onTagClickListener="@{() -> presenter.onLoadMore()}" * 如果方法需要有返回值,那么你的表达式或者方法的返回类型一定要和属性的返回类型一致,否则会编译不通过。 ## 其他布局细节 ### Import导包 `<data>`标签里面还支持import语法,具体的格式是下面这样,跟java代码里面差不多: <data> <import type="android.view.View"/> </data> 导包之后就可以使用它的一些静态方法或者静态变量,例如`View.GONE`等等,甚至可以导入`TextUtils`来判断字符串非空。但由于可读性和可维护性方面的问题,我们在项目中应该**禁止**xml中使用任何条件语句。 关于*import*更详细的资料请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#imports) ### Variables变量 `variable`标签会为每个声明的变量赋初始值,如果是引用类型则初始值为null,int类型是0,等等。 xml中有一个隐藏的变量名`context`,它对应的值是布局根元素的`getContext`方法获取的上下文对象。如果你的xml中显式声明`context`变量,那么这个变量会被你的声明所覆盖。 关于*variable*更详细的资料请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#variables)。 ### 自定义生成的绑定类名 默认情况下,根据布局文件的名称生成一个Binding类,以大写字母开头,删除下划线`_`并大写后面的字母,然后后缀“Binding”。 该类将放置在模块包下的数据绑定包中。 例如,布局文件contact_item.xml将生成ContactItemBinding。 如果模块包是com.example.my.app,那么它将被放置在com.example.my.app.databinding中。 绑定类可以通过调整`<data>`元素的`class`属性来重命名或放置在不同的包中。 例如: <data class="ContactItem"> ... </data> 以上设置会把xml生成的对应绑定类重命名为ContactItem,而不是XXXBinding。`class`属性还可以设置相对或者完整包名,具体请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#custom_binding_class_names) ### Inclub标签绑定 略。 [官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#includes)。 ### 布局中可以使用的表达语言 从可读性和可维护性的角度触发,我们项目中禁止布局文件中做任何逻辑和业务相关的表达式运算,绑定的字段一般就是最终的输出。作为了解,可以查看一下[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#expression_language)。 # 绑定的数据对象 从上面的介绍得知,我们可以把普通的java对象(POJO)或者java bean对象(BEAN)绑定到属性里面,但是当这些对象的属性发生变化的时候,页面是不会随之发送变化的。如果需要属性变化马上反应到视图上,就要使用绑定库提供的`Data Objects`。有以下三种实现方法提供: * 继承BaseObservable,为需要实时变更的get方法加上`@Bindable`注解,编译器会自动为该方法生成对应的id存放在BR里面(类似R文件)。对应的set方法需要调用`notifyPropertyChanged(id)`,例如: 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); } } * 使用ObservableField和他的同胞 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.使用它们来替代你的类中需要实时变化的字段类型,不需要额外设置get set方法,但修饰符需要是`public final`。例如: private static class User { public final ObservableField<String> firstName = new ObservableField<>(); public final ObservableField<String> lastName = new ObservableField<>(); public final ObservableInt age = new ObservableInt(); } * 使用Observable Collections,例如 ObservableArrayMap 和 ObservableArrayList。在xml中可以使用符号[]访问。但直接使用集合来绑定数据,还是会破坏可读性。在类中使用的话和ObservableField无异。所以有需要了解的请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#observable_collections)。 # 生成的绑定类浅析 前面已经介绍过,编译器会为每一个使用数据绑定的xml生成一个继承`android.databinding.ViewDataBinding`的绑定类。绑定类处理好所有该布局文件的属性绑定和事件处理,并且提供对外的设置绑定对象的方法。这个类可以重命名,可以自定义包名等等,这些在之前已经描述过。 ## 创建绑定 在布局被`inflate`之后应该尽快绑定,避免对View的操作造成绑定失败。绑定的方法一般有以下几种: * 用生成的绑定类`inflate`,会同时执行好布局的`inflate`和绑定,无须额外操作: MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater); MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false); * 如果布局是使用不同的机制来进行`inflate`的,可以使用绑定类的`bind`方法进行绑定: MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot); * 或者都可以直接用`DataBindingUtil.inflate`方法进行创建布局、生成绑定,相比具体的绑定类,只是多了参数,返回的类型是`<T extends ViewDataBinding>`: ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent); * 在Activity中可以使用`DataBindingUtil.setContentView`方法类代替activity原有的`setContentView`方法: ActivityMain2Binding binding = DataBindingUtil.setContentView(activity.this, R.layout.activity_main2); ## 为带id的元素生成引用 简直就是**福音**!!!绑定类会为所有布局中带id的元素生成引用,例如在xml中写了一个TextView是id是`text`,绑定类里面就会生成一个`public final TextView text;`的引用,从此告别`findViewById`,我简直都要哭了~生成的字段名的规则是首字母小写,下划线去除,下划线后的首字母大写。 ## 为变量生成代码 绑定类会为定义在`<variable>`标签的对象生成对应的`get` `set`方法,如果有涉及到事件处理,还会根据事件传入方式生成一些监听器或者一些事件回调类: // variables @Nullable private com.healthmall.library.app.databinding.DataBindingListItemInfoVM mItem; @Nullable private com.healthmall.library.app.databinding.ListPresenter mPresenter; @Nullable private final android.view.View.OnClickListener mCallback4; // values // listeners private OnClickListenerImpl mPresenterOnClickViewAndroidViewViewOnClickListener; ## ViewStub绑定 提供了ViewStubProxy来解决ViewStub的绑定问题,具体请看[ViewStubProxy文档](https://developer.android.com/reference/android/databinding/ViewStubProxy.html)。 ## 高级特性 ### 动态绑定 编译器会为xml的每个变量生成对应的id,绑定类会为每个变量生成对应的get、set方法,但有时候我们没办法确定具体的id和具体的对象,譬如我们在编写adapter基类,假设我们有一个统一的id `BR.item` ,但所有列表对象都基本不一致。所以需要使用ViewDataBinding(绑定类的基类)提供的`setVariable(bindId, bindObject)`方法来进行动态绑定。 @Override public final void onBindViewHolder(BaseBindingViewHolder holder, int position) { // 绑定数据 holder.bind.setVariable(BR.item, list.get(position)); holder.bind.executePendingBindings(); } ### 绑定立即生效 当变量或可观察到的变化时,绑定将被安排在下一帧之前改变。 然而,有时候,绑定必须立即执行。 要强制执行,请使用`executePendingBindings`方法。 ### 线程安全 只要不是集合,您可以在后台线程中更改数据模型。 数据绑定将在评估时本地化每个变量/字段以避免任何并发问题。 # 属性设置器 属性设置器是编译器连接代码和布局的一套规则,它决定了绑定类如何生成。除了默认的规则以外,数据绑定框架还提供了自定义的方法,来帮助我们按照自己的规则去建立绑定。作为开发者而言,个人认为这是我们最应该熟悉的部分,没有它上面的语法都不成立。 ## 属性自动设置器 对于绑定的任意一个属性(忽略namespace),它会根据自己的属性名去寻找对应的setter方法,而不管对应控件中是否已经声明相同名称的属性字段。例如TextView里面的`android:text`属性,关联的表达式会被设置到`setText()`方法里面。例如我们的图片显示控件ThumbnailDraweeView,虽然没有一个名为`imageURI`的属性,但有`setImageURI`方法,所以我们可以轻松的使用`imageURI`属性去为控件设置要显示的图片: <com.gzdxjk.healthmall_android_library.widget.facebook.ThumbnailDraweeView android:id="@+id/image" android:layout_width="60dp" android:layout_height="60dp" android:layout_margin="15dp" app:imageURI="@{item.url}" /> 有了这个机制以后,我们可以为任何setter方法在xml中创造对应的属性,而不需要预先在控件中定义,包括事件处理,这也是极大的增加了灵活性。例如: <com.gzdxjk.healthmall_android_library.widget.LoadingView android:id="@+id/loading" android:layout_width="match_parent" android:layout_height="match_parent" app:noDataMessage="@{flowlist.noDataMsg}" app:onRefreshListener="@{() -> presenter.onStart(loading, context, flowlist)}"> 以上示例里面可以看到,我们项目中的LoadingView并没有`noDataMessage`这个属性,也没有`onRefreshListener`这个属性,但通过自动设置器,相应的表达式还是正确的设置到了对应的方法里面。 ## 重命名设置器 有的属性对应的setter方法会跟属性名不一致,例如ImageView的`android:tint`属性,它对应的方法是`setImageTintList`,而不是`setTint`。这时候可以使用`@BindingMethods`注解来重命名setter,它是一个类注解: @BindingMethods({ @BindingMethod(type = "android.widget.ImageView", attribute = "android:tint", method = "setImageTintList"), }) ## 自定义设置器 假如存在的属性没有对应的setter方法,那么可以编写一个由`@BindingAdapter`注解的静态方法来自定义该属性的行为,例如`android:paddingLeft`属性,并没有一个`setPaddingLeft`方法让自动设置器去设置它对应的表达式,这时候可以: @BindingAdapter("android:paddingLeft") public static void setPaddingLeft(View view, int padding) { view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); } 假如自动设置器提供的方法不满足你的需求,你也可以使用自定义设置器去覆盖系统提供的方法。 BindingAdapter还可以接收多个参数,对应到一个方法里面。例如我们的流式布局FlowLayout中的自定义设置器: @BindingAdapter({"bind:child_layout", "bind:items"}) public static void bindTextItem(FlowLayout flowLayout, int childLayoutId, List<String> itemList) { flowLayout.addTextTag(childLayoutId, itemList); } 当布局中的`child_layout`和`items`属性同时存在(忽略namespace)并且它们的表达式返回类型和定义好的方法一致时,将会调用`bindTextItem`方法: <com.gzdxjk.healthmall_android_library.widget.FlowLayout android:id="@+id/flow_layout" android:layout_width="match_parent" android:layout_height="300dp" app:child_layout="@{@layout/layout_textview}" app:items="@{flowlist.list}" app:onTagClickListener="@{(view,tag,position) -> presenter.addListItem(position)}" /> BindingAdapter还允许其处理方法使用变化前的值,具体的格式应该是旧值在前,新值在后: @BindingAdapter("android:paddingLeft") public static void setPaddingLeft(View view, int oldPadding, int newPadding) { if (oldPadding != newPadding) { view.setPadding(newPadding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); } } 对于一个接口有多个方法的情况,必须为每个方法重写一个接口,并且在自定义设置器中一一对应,由于情况比较特殊,具体实例请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#custom_setters)。 # 数据类型转换 同样,为了可读性考虑,禁止在xml中使用数据类型转换。