参考
https://developer.android.com/topic/libraries/data-binding
https://blog.csdn.net/qiang_xi/article/details/74347880
配置
l 设备需要是Android4.0 api14及以上版本,
l 项目根目录的build.gradle中的classpath 'com.android.tools.build:gradle:xxx' 版本需要1.5.0及以上。
l 在module的build.gradle中
android { dataBinding { enabled = true } }
注意1:即使app module不直接使用data binding,如果app module依赖的库使用了 data binding,那么app module也必须配置。
注意2:Arrays and a generic type, such as the Observable class, might incorrectly display errors.
注意3:不用添加依赖,添加完上边的后就会直接引入几个库:
布局和绑定表达式
你可以在布局文件中写入 布局 和 绑定表达式,如下所示:
<?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>
绑定布局和普通的布局有些许不同,
不同之处在:绑定布局根元素是layout,在layout内部接着data元素和布局。
- l data元素:就是用来声明一些绑定参数,例如要绑定的bean。
- l 布局:就是普通布局中的布局,只不过在具体属性的设置用@{bean.xxx}来赋值
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}" />
另外:
- l data binding库会自动生成将布局中的视图与数据对象绑定所需的类。
- l 布局表达式应保持小而简单,因为不能测试、ide对此的支持是有限的。
数据类的定义规则
public class User { public final String firstName; public final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
public class User { private final String firstName; private final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } }
这两种定义方式都是可以的,
在布局中只有一种写法android:text="@{user.firstName}",但在查找对应的值时,
- l 会去查找变量名对应的字段,比如firstName
- l 也可以找此字段的get方法,比如getFirstName();
- l 或者此变量名 有对应的方法也可以,比如firstName();
- l 当然不一定要用final修饰,只不过User不是一个可观察的对象,所以user数据发生变化并会不通知相关ui进行更新。
Binding data
上边定义了一个布局和一个数据类,那么想要显示对应的ui肯定还是需要进行 数据对象的创建 及 和布局的绑定设置的。
系统为每个绑定布局文件生成一个绑定类。 默认情况下,类的名称基于布局文件的名称,将其转换为Pascal大小写并向其添加Binding后缀。 上面的布局文件名是activity_main.xml,因此相应的生成类是ActivityMainBinding。 此类包含从布局属性(例如,user 变量)到布局视图的所有绑定,并且知道如何为绑定表达式分配值。
创建绑定的推荐方法是在inflating 布局的同时进行绑定,例如 如以下示例所示:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); User user = new User("Test", "User"); binding.setUser(user); }
或者,您可以使用LayoutInflater获取绑定对象,如以下示例所示:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
如果你是在Fragment、ListViewAdapter、RecyclerViewAdapter等中使用data binding的话,可以使用下边方法:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
表达式语法
操作符
The expression language looks a lot like expressions found in managed code.
You can use the following operators and keywords in the expression language:
- Mathematical + - / * %
- String concatenation +
- Logical && ||
- Binary & | ^
- Unary + - ! ~
- Shift >> >>> <<
- Comparison == > < >= <= (Note that < needs to be escaped as <)
- instanceof
- Grouping ()
- Literals - character, String, numeric, null
- Cast
- Method calls
- Field access
- Array access []
- Ternary operator ?:
Examples:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
default
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.name,default=1}" />
default只在layout编辑器中有效,效果和tools:text=”1”一样。
不能使用的操作
以下操作是不能使用的:
- l this
- l super
- l new
- l Explicit generic invocation
空合并运算符??
空合并运算符??
android:text="@{user.displayName ?? user.lastName}"
如果不是null则取前边的,否则取后边的,等效于下边:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
表达式可以使用以下格式来引用类中的属性,以下格式对于字段,getter和ObservableField对象是相同的:
android:text="@{user.lastName}"
自动避免空指针
生成的databinding代码会自动的检查null值,来避免空指针。
例如, @{user.name},如果user为null,那么最终就取name的默认值,即String的默认值,即null,
如果@{user.age},则会取age的默认值,即int的默认值,为0;
集合
arrays, lists, sparse lists, and maps这些集合都可以通过[ ]来访问。
<data> <import type="android.util.SparseArray"/> <import type="java.util.Map"/> <import type="java.util.List"/> <variable name="list" type="List<String>"/> <variable name="sparse" type="SparseArray<String>"/> <variable name="map" type="Map<String, String>"/> <variable name="index" type="int"/> <variable name="key" type="String"/> </data> … android:text="@{list[index]}" … android:text="@{sparse[index]}" … android:text="@{map[key]}"
注意:要使XML语法正确,必须转义 < 。例如:您必须编写List<;String>,而不是List<String>。
你也可以使用@{map.key}等效于@{map[key]}
引号和字符串常量
你可以使用,单引号在外边,双引号在里边:
android:text='@{map["firstName"]}'
你也可以使用双引号在外边,back quotes(键盘左上角的字符`)在里边:
android:text="@{map[`firstName`]}"
Resources
You can access resources in an expression by using the following syntax:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
也可以使用格式化字符串:
android:text="@{@string/nameFormat(firstName, lastName)}"
nameFormat为字串的id名后边括号里的是具体的实参。
当然也可以引用strings.xml中的:
android:text="@{@string/format_test(@string/activity_lifecycle)}"
一些资源需要显式类型求值,如下表所示:
Type |
Normal reference |
Expression reference |
String[] |
@array |
@stringArray[index] |
int[] |
@array |
@intArray[index] |
TypedArray |
@array |
@typedArray |
Animator |
@animator |
@animator |
StateListAnimator |
@animator |
@stateListAnimator |
color int |
@color |
@color |
ColorStateList |
@color |
@colorStateList |
事件处理
data binding允许你写表达式来处理那些从view分发的事件,比如onClick事件,
事件属性名称由侦听器方法的名称确定,但有几个例外。
例如,View.OnClickListener有一个 onClick(View)方法,因此在布局中的属性就叫做 android:onClick。
你可以使用两种方式处理事件:
- l 方法引用
- l 监听器绑定
方法引用
java8双引号::
在表达式中,可以引用符合侦听器方法签名的方法。
事件可以直接绑定到处理程序方法,类似于可以将android:onClick分配给Activity中的方法的方式。 但和View onClick属性相比,一个主要优点是表达式是在编译时处理的,因此,如果该方法不存在或其签名不正确时,则会收到编译时错误。
方法引用和侦听器绑定之间的主要区别在于,方法引用的 实际的侦听器实现是在绑定数据时创建的,而不是在事件触发时创建的。 如果您希望在事件发生时执行表达式,则应使用侦听器绑定。
在创建处理器方法时,方法的参数必须和要和要监听的事件一致,比如下边监听的是onClick,View.OnClickListener有一个 onClick(View)方法,那么在创建事件处理器时就需要创建的方法参数中也要带view:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
<?xml version="1.0" encoding="utf-8"?> <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>
监听器绑定
java8的lambda表达式
侦听器绑定是事件发生时运行的绑定表达式。 它们类似于方法引用,但是它们使您可以运行任意数据绑定表达式。 适用于Gradle 2.0版及更高版本的Android Gradle插件提供了此功能。
onClick
- l 在上边方法引用中,方法的参数必须与事件侦听器的参数匹配。
- l 而在侦听器绑定中,只有您的返回值必须与侦听器的期望返回值匹配(除非期望返回void)。 例如,假设以下具有onSaveClick()方法的presenter类:
public class Presenter {
public void onSaveClick(Task task){}
}
<?xml version="1.0" encoding="utf-8"?> <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>
- l 在表达式中使用回调时,数据绑定会自动为事件创建并注册必要的监听器。当视图触发事件时,数据绑定会对给定表达式求值。
- l 与常规绑定表达式一样,在对这些监听器表达式求值时,仍会获得数据绑定的 Null 值和线程安全。
在上面的示例中,我们尚未定义传递给onClick(View)的view参数。 侦听器绑定为侦听器参数提供了两种选择:您可以忽略该方法的所有参数,也可以全部命名。 如果您想命名参数,则可以在表达式中使用它们。 例如,上面的表达式可以编写如下:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
或者,如果要在表达式中使用参数,则可以按以下方式工作:
public class Presenter { public void onSaveClick(View view, Task task){} }
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
多个参数的lambda表达式
您可以使用具有多个参数的lambda表达式:
public class Presenter { public void onCompletedChanged(Task task, boolean completed){} }
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content" android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
返回值不是void
public class Presenter { public boolean onLongClick(View view, Task task) { } }
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于null 对象而无法对表达式求值,则数据绑定将返回该类型的默认值。 例如,对于引用类型,为null,对于int为0,对于布尔值为false,等等。
其他
- l 如果需要将表达式与谓词(例如,三元)一起使用,则可以使用void作为符号。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
- l 尽量不要把业务逻辑都写在布局文件中,应该保持布局文件的简洁,而把具体的复杂的业务逻辑写在具体的类的方法中。
import, variable, include,ViewStub
import
- l import类似于java代码中的import,
- l 默认导入java.lang下的一些包
DataBinding已经默认帮我们导入了String类,Integer类等java.lang下的基本数据类型
- l import的类可以用在布局中,也可以用在variable
<data> <import type="android.view.View"/> </data> <TextView android:text="@{user.lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
导入的类还可以用于强转
<TextView android:text="@{((User)(user.connection)).lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
导入的类还可以使用其静态常量和静态方法
<data> <import type="com.example.MyStringUtils"/> <variable name="user" type="com.example.User"/> </data> … <TextView android:text="@{MyStringUtils.capitalize(user.lastName)}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
类型别名
如果导入的类名有重复的,则可以进行别名来避免混淆:
<import type="android.view.View"/> <import type="com.example.real.estate.View" alias="Vista"/>
variable
<data> <variable name="user" type="com.example.User"/> <variable name="image" type="Drawable"/> <variable name="note" type="String"/> </data>
- l 当针对各种配置(例如,横向或纵向)有不同的布局文件时,将合并变量。 这些布局文件之间不得有冲突的变量定义。
- l 对于每个所描述的变量,生成的绑定类都有一个setter和getter。 变量将使用默认的托管代码值,直到调用setter为止,引用类型为null,int为0,布尔值为false,等等。
Context对象
l databinding会自动导入Context对象,context的值是根视图的getContext()方法获取的,所以我们可以在布局中直接使用context,而不用导入或声明。
但是如果我们自己通过variable标签定义了一个name为context的对象,那么会覆盖掉系统提供的context。
include
数据传递
include类似于普通布局中使用的include,但有一点不同是,
如果include的也是一个绑定布局,并且有一个参数的类型和第一个布局一样,那么我们把参数传递进去,只需要把实际数据设置给第一个绑定类,那么include进来的布局也会显示对应的数据。
- !ViewStub也支持!
- 另外,include不支持自定义BindingMethods,BindingAdapter。
例子
activity_main2.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="user" type="com.example.jetpackdemo.databingding.User" /> <variable name="index" type="int" /> <variable name="handler" type="com.example.jetpackdemo.databingding.MyHandlers" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{@string/test(index)}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.name,default=1}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{String.valueOf(user.age)}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.sex}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="@{(view)->handler.onClickFriend(view)}" android:text="Hello World!" /> <include app:user2="@{user}" layout="@layout/activity_main3" /> </LinearLayout> </layout>
app:user2="@{user}"中的user2就是include的布局中定义的参数名,user是此布局定义的参数名。
activity_main3.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="user2" type="com.example.jetpackdemo.databingding.User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user2.name,default=1}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{String.valueOf(user2.age)}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user2.sex}" /> </LinearLayout> </layout>
ActivityMain2Binding activityMain2Binding = DataBindingUtil.setContentView(this, R.layout.activity_main2); User user = new User("haha", 18, "女"); activityMain2Binding.setUser(user);
最终我们只需要把数据设置给ActivityMain2Binding ,那么这两个xml都会显示对应的数据。
include上加上id
还用上边例子,如果在include上加上id,那么就可以直接访问include的绑定布局对象,而不需要再去用DataBingdingUtil去获取。
<include android:id="@+id/databinding2" layout="@layout/databinding2" app:user2="@{user}" />
databinding1Binding.databinding2
分析一下生成的代码就明白了:
Databinding1BindingImpl继承自Databinding1Binding
public Databinding1BindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) { this(bindingComponent, root, mapBindings(bindingComponent, root, 9, sIncludes, sViewsWithIds)); } private Databinding1BindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) { super(bindingComponent, root, 2 , (android.widget.Button) bindings[1] , (com.example.jetpackdemo.databinding.Databinding2Binding) bindings[8] , (android.widget.LinearLayout) bindings[0] , (android.widget.TextView) bindings[5] , (android.widget.TextView) bindings[4] , (android.widget.TextView) bindings[6] );
Databinding1Binding是绑定布局生成的绑定类
protected Databinding1Binding(Object _bindingComponent, View _root, int _localFieldCount, Button btRefresh, Databinding2Binding databinding2, LinearLayout root, TextView tvAge, TextView tvName, TextView tvSex) { super(_bindingComponent, _root, _localFieldCount); this.btRefresh = btRefresh; this.databinding2 = databinding2; setContainedBinding(this.databinding2); this.root = root; this.tvAge = tvAge; this.tvName = tvName; this.tvSex = tvSex; }
调用构造函数时,传递的root view布局肯定是inflate过的,也就是说布局的层级已经有了。
可以看到对在构造函数中直接对Databinding2Binding 进行了赋值,那么这个对象是从哪创建的,
关键就在mapBindings这个方法,这个方法就是解析view及其子view上设置的tag(这个tag是自动添加上的),并把解析出来的对象放到bindings数组的合适的位置。
其中就会调用DataBindingUtil.bind方法(不管include的布局是否是绑定布局),如果include的布局是绑定布局,那么就会获得绑定对象。
ViewStub
在绑定布局中使用ViewStub,会在生成的绑定类中用ViewStubProxy来代替ViewStub,
因为ViewStub本质上是个不可见的view,所以ViewStub引用的布局会在ViewStub inflate后才生成绑定对象。
注意:
- ViewStub也支持 数据传递
只是需要注意的是,是在ViewStub inflate之后才支持,也就是说需要在inflate之后才可以设置要传递的值。
- ViewStub不支持自定义BindingMethods,BindingAdapter
和可观察的对象一起使用
可观测性是指对象通知其他人其数据变化的能力。 数据绑定库 使您可以观察对象,字段或集合。
任何bean数据对象都可以用于数据绑定,但是修改对象不会自动导致UI更新。 数据绑定可用于使您的数据对象在数据更改时通知其他对象(称为侦听器)。
有三种不同类型的可观察类:对象object,字段field 和集合collection。
当这些可观察数据对象之一绑定到UI且数据对象的属性更改时,UI将自动更新。
Observable fields
注意:Android Studio 3.1及更高版本允许您用LiveData对象替换Observable fields,这为您的应用程序提供了更多好处。
下边是一些基本数据类型对应的Observable类,
- l ObservableBoolean
- l ObservableByte
- l ObservableChar
- l ObservableShort
- l ObservableInt
- l ObservableLong
- l ObservableFloat
- l ObservableDouble
- l ObservableParcelable
可观察字段是具有单个字段的自包含可观察对象。 原始版本避免在访问操作期间装箱和拆箱。 要使用此机制,请使用Java编程语言创建一个public final属性,或者使用Kotlin创建一个只读属性,如以下示例所示:
private static class User { public final ObservableField<String> firstName = new ObservableField<>(); public final ObservableField<String> lastName = new ObservableField<>(); public final ObservableInt age = new ObservableInt(); }
要访问该字段值,请使用set()和get()访问器方法,如下所示:
user.firstName.set("Google"); int age = user.age.get();
Observable collections
一些应用程序使用动态结构来保存数据。 可观察的集合Observable collections允许使用key(键)访问这些结构。
ObservableArrayMap
当key(键)是引用类型(例如String)时,ObservableArrayMap类非常有用,如以下示例所示:
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>(); user.put("firstName", "Google"); user.put("lastName", "Inc."); user.put("age", 17);
在布局中,可以使用字符串键找到map中的值,如下所示:
<data> <import type="android.databinding.ObservableMap"/> <variable name="user" type="ObservableMap<String, Object>"/> </data> … <TextView android:text="@{user.lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:text="@{String.valueOf(1 + (Integer)user.age)}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
ObservableArrayList
当键为整数时,ObservableArrayList类非常有用,如下所示:
ObservableArrayList<Object> user = new ObservableArrayList<>(); user.add("Google"); user.add("Inc."); user.add(17);
在布局中,可以通过索引访问列表,如以下示例所示:
<data> <import type="android.databinding.ObservableList"/> <import type="com.example.my.app.Fields"/> <variable name="user" type="ObservableList<Object>"/> </data> … <TextView android:text='@{user[Fields.LAST_NAME]}' android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}' android:layout_width="wrap_content" android:layout_height="wrap_content"/>
Observable objects
实现Observable接口的类允许注册要在可观察对象上进行属性更改通知的侦听器。
Observable接口具有添加和删除侦听器的机制,但是您必须确定何时发送通知。 为了简化开发,数据绑定库提供了BaseObservable类,该类实现了侦听器注册机制。
实现BaseObservable的数据类负责在属性更改时发出通知。这是通过为getter分配Bindable注解并在setter中调用notifyPropertyChanged()方法来完成的,如以下示例所示:
public 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(com.eallcn.mlw.rentcustomer.BR.firstName); } public void setLastName(String lastName) { this.lastName = lastName; notifyPropertyChanged(com.eallcn.mlw.rentcustomer.BR.lastName); } }
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}" />
- l 数据绑定在模块包中生成一个名为BR的类,其中包含用于数据绑定的资源的ID。
Bindable注解在编译期间会在BR类文件中生成一个条目。可以在xml中使用。
- l 如果不能更改数据类的基类,那么我们可以impletement Observable接口,并自己通过使用PropertyChangeRegistry对象来以高效地注册和通知侦听器。
Bindable注解
@Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) // this is necessary for java analyzer to work public @interface Bindable { String[] value() default {}; }
Bindable 注解应该应用于一个Observable 类的任何getter访问方法。Bindable注解在编译期间会在BR类文件中生成一个条目。
Bindable注解可以设置一个依赖属性列表。如果列出的属性中有一个更改通知,那么Bindable注解的这个值也会被认为是dirty的,并会被刷新。例如:
@Bindable public String getName() { return name; } public void setName(String name) { this.name = name; notifyPropertyChanged(com.example.jetpackdemo.BR.name); } @Bindable public int getAge() { return age; } public void setAge(int age) { this.age = age; notifyPropertyChanged(com.example.jetpackdemo.BR.age); } @Bindable({"name", "age"}) public String getNameAndAge() { return name + "-" + age; }
在BR中会生成一个名为nameAndAge的item,那么在xml中就也可以使用。
每当age或name有更改通知时,nameAndAge也会被认为是dirty的。但这并不意味着Observable.OnPropertyChangedCallback.onPropertyChanged(Observable, int)将得到BR.nameAndAge的通知,只有包含nameAndAge的绑定表达式将被刷新。
和LiveData一起使用
LiveData是生命周期感知的对象,所以如果在xml的绑定表达式中引入了LiveData,那么就必须在生成的绑定对象里设置LifecycleOwner,否则LiveData不能被观察,对LiveData的数据更新也不会传播到UI。
@MainThread public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner)
此方法是ViewDataBinding类的成员方法。
自定义生成的绑定类
生成的绑定类将布局变量与布局中的视图链接。 绑定类的名称和包可以自定义。 所有生成的绑定类都从ViewDataBinding类继承。
xml中带有id的view
对于有id的view,会在生成的绑定类里生成一个id名字的view的引用,我们可以直接通过此引用直接操作此view。
例如:
<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}" android:id="@+id/firstName"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}" android:id="@+id/lastName"/> </LinearLayout> </layout>
那么在生成的绑定类里,就会生成两个TextView的变量,一个叫firstName,一个叫lastName,他们都是public final修饰的。
立即绑定executePendingBindings
当变量或可观察对象发生更改时,绑定将安排在下一帧之前更改。
但是,有时绑定必须立即执行。 要强制执行,请使用executePendingBindings()方法。
在RecyclerView的adapter中的onBindViewHolder使用时
在RecyclerView的adapter的onBindViewHolder中一般需要使用executePendingBindings()
由于不使用 executePendingBindings()时,数据设置到ui上需要等到下一帧,而此时当要绑定一个数据的时候,RecyclerView 会调用 onBindViewHolder,让你去准备测量这个布局,而我们的ui还没设置数据,所以此时ui可能就会闪烁一下。
不使用executePendingBindings()有可能还会导致图片闪烁,
原因可能是 由于不使用 executePendingBindings()时,数据设置到ui上需要等到下一帧,而如果在此期间设置数据多次,那么最后ui显示时会突然显示几次。
Dynamic Variables
生成的绑定类是继承自ViewDataBinding,
有时并不知道绑定对象是哪个具体的绑定类的对象,那么此时可以使用ViewDataBinding的一个方法setVariable,这个方法是每个生成的绑定类都会实现的,那么调用此方法就相当于调用具体的绑定类的对象的某个set方法。
public boolean setVariable(int variableId, @Nullable Object variable)
variableId:可以使用生成的BR类中的某个id。
Background Thread
您可以在后台线程中更改数据模型,但前提是这个模型不是集合。数据绑定会在求值过程中对每个变量/字段进行本地化,以避免出现并发问题。
自定义生成的绑定类名
默认情况下,将基于布局文件的名称生成绑定类,该绑定类以大写字母开头,去掉下划线(_)大写下划线之后的字母并在后边加上Binding后缀。
该类位于模块包下的databinding包中。 例如,布局文件contact_item.xml生成ContactItemBinding类。 如果模块包是com.example.my.app,则将绑定类放置在com.example.my.app.databinding包中。
可以通过绑定布局中的data的class属性来修改类名或包名。
<data class="ContactItem"> … </data>
这种方式可以修改类名,包还是com.example.my.app.databinding
<data class=".ContactItem"> … </data>
这种方式可以修改类名,包变成com.example.my.app
<data class="com.example.ContactItem"> … </data>
这种方式可以修改包名和类名,变成com.example包下的ContactItem类
Binding adapters
Binding adapters负责进行适当的框架调用以设置值。
- l 一个示例是设置属性值,例如调用setText()方法。
- l 另一个示例是设置事件侦听器,例如调用setOnClickListener()方法。
数据绑定库 允许您指定调用的方法来设置值,提供自己的绑定逻辑 以及 使用适配器指定返回对象的类型。
指定属性setter方法
每当绑定值更改时,生成的绑定类都会 使用绑定表达式 调用View对应的setter方法。 您可以允许数据绑定库自动确定方法,也可以显式声明该方法,或提供自定义逻辑来选择方法。
!ViewStub不支持、include不支持!
自动选择setter方法
1. 对于名为example的属性,databinding库会自动尝试查找setExample(arg)方法,查找时会考虑 在布局中写的绑定表达式返回的类型 是否和setExample接收的参数类型是否匹配。
在搜索方法时不考虑属性的名称空间,在搜索方法时仅使用属性名称和类型。
例如:现有一个绑定表达式 android:text="@{user.name}",那么绑定库就会去查找此view的setText方法,
如果user.getName()返回的是String,那么会去找接收的参数和String兼容的setText方法;
而如果user.getName()返回的是int,那么会去找接收的参数和int兼容的setText方法;
当然我们可以通过强转来指定调用的方法。
2. 即使没有给定名称的view属性,数据绑定也可以工作,前提是此view得有此名称的setter方法。
比如support库中的Drawerlayout,它是没有什么自定义的属性的,但有很多setter方法,那么如下使用:
<android.support.v4.widget.DrawerLayout android:layout_width="wrap_content" android:layout_height="wrap_content" app:scrimColor="@{@color/scrim}" app:drawerListener="@{fragment.drawerListener}">
那么会自动的查找到对应的setScrimColor(int) 和 setDrawerListener(DrawerListener)。
指定自定义的方法名BindingMethods
一些view的属性的setter方法可能和属性名并无关联,这种情况下就可以使用@BindingMethods 注解来处理。
@Target({ElementType.TYPE}) public @interface BindingMethods { BindingMethod[] value(); }
@Target(ElementType.ANNOTATION_TYPE) public @interface BindingMethod { /** * 属性关联的view的class */ Class type(); /** * 如果属性是android自带的属性,那么此字符串就是android:xxx; 如果是自定义的属性,那么字符串就是属性名即可,xxx,但在xml中app:xxx !可以没有定义过! */ String attribute(); /** * @return The method to call to set the attribute value. */ String method(); }
BindingMethods是可以添加到应用程序中任何类的注解。
例如:
@BindingMethods({ @BindingMethod(type = ImageView.class, attribute = "android:tint", method = "setImageTintList"), })
大部分 官方的view的属性对应的setter不匹配的属性,databinding库已经添加了很多BindingMethods注解,比如TextViewBindingAdapter对textview的一些不匹配的属性添加了BindingMethods注解。
指定自定义方法BindingAdapter
和上边的指定方法名不一样,此方式是静态方法,而上边的是成员方法。
比如有些属性是没有setter方法的,就是说没有 方法名匹配 和 参数兼容的方法,比如android:paddingLeft,但是view是有setPadding(left, top, right, bottom)方法的,那么此时就可以在一个静态方法上使用BindingAdapter注解。
@Target(ElementType.METHOD) public @interface BindingAdapter { /** * @return The attributes associated with this binding adapter. 接收的参数列表,如果是Android自带的需要android:xxx; 如果是自定义的属性,则直接写属性,但在xml中app:xxx !可以没有定义过! */ String[] value(); /** * 是否需要每个属性都需要有绑定表达式。 当false时,属性列表中至少有一个是有绑定表达式的,那么此注解的方法将被调用,那些没有绑定表达式的属性会传递java的默认值。 必须注意确保默认值不要与有效的XML值混淆。 * * @return whether or not every attribute must be assigned a binding expression. The default value is true. */ boolean requireAll() default true; }
@BindingAdapter("android:paddingLeft") public static void setPaddingLeft(View view, int padding) { view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); }
第一个参数View 是与属性关联的视图的类型。
第二个参数确定给定属性在绑定表达式中接受的类型。
databinding库已经提供了很多默认的BindingAdapter,比如ViewBindingAdapter中的,TextViewBindingAdapter中的,
当发生冲突时,您定义的BindingAdapter将覆盖databinding库提供的默认适配器。
requireAll
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
@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); } }
oldValue
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()); } }
和Event handlers一起使用
事件处理程序 只能与具有一种抽象方法的接口或抽象类一起使用,如以下示例所示:
<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>
@BindingAdapter("android:onLayoutChange") public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue, View.OnLayoutChangeListener newValue) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (oldValue != null) { view.removeOnLayoutChangeListener(oldValue); } if (newValue != null) { view.addOnLayoutChangeListener(newValue); } } }
当一个侦听器有多种方法时,必须将其拆分为多个侦听器。
例如 View.OnAttachStateChangeListener 有两个方法: onViewAttachedToWindow(View) 和onViewDetachedFromWindow(View).
那么就需要把这两个方法拆分出来:
@TargetApi(VERSION_CODES.HONEYCOMB_MR1) public interface OnViewDetachedFromWindow { void onViewDetachedFromWindow(View v); } @TargetApi(VERSION_CODES.HONEYCOMB_MR1) public interface OnViewAttachedToWindow { void onViewAttachedToWindow(View v); }
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false) public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) { if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) { OnAttachStateChangeListener newListener; if (detach == null && attach == null) { newListener = null; } else { newListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { if (attach != null) { attach.onViewAttachedToWindow(v); } } @Override public void onViewDetachedFromWindow(View v) { if (detach != null) { detach.onViewDetachedFromWindow(v); } } }; } OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener); if (oldListener != null) { view.removeOnAttachStateChangeListener(oldListener); } if (newListener != null) { view.addOnAttachStateChangeListener(newListener); } } }
对象类型转换
这里所说的转换是指使用绑定表达式时,存在的类型不一致的处理。而普通布局是不存在这种问题的。
自动转换
从绑定表达式返回Object时,库将选择用于设置属性值的方法。 将对象强制转换为所选方法的参数类型。
在使用ObservableMap类存储数据的应用程序中,此行为很方便,如以下示例所示:
<TextView android:text='@{userMap["lastName"]}' android:layout_width="wrap_content" android:layout_height="wrap_content" />
自定义转换BindingConversion
在某些情况下,需要在特定类型之间进行自定义转换。
例如,视图的android:background属性需要Drawable,但是指定的颜色值为整数。 以下示例显示了一个期望使用Drawable的属性,但是提供了一个整数:
<View android:background="@{isError ? @color/red : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
每当需要Drawable并返回整数时,应该将int转换为ColorDrawable。
那么就可以使用带有BindingConversion注解的静态方法完成转换,如下所示:
@BindingConversion public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); }
@Target({ElementType.METHOD}) public @interface BindingConversion { }
但是,绑定表达式中提供的值类型必须一致。 不能在同一表达式中使用不同的类型,如以下示例所示:
<View android:background="@{isError ? @drawable/error : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>