2. Data Binding 中的布局
2.1. 编写 data binding 表达式
2.1.1 DataBinding 的布局文件与以前的布局文件有一点不同。它以一个 layout 标签作为根节点,里面包含一个 data 标签与 view 标签。view 标签的内容就是不使用 data binding 时的普通布局文件内容。例子如下:
<?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>
2.1.2 在 data 标签中定义的 user 变量,可以在布局中当作属性来使用,用来写一些和java代码中表达式类似的"databinding表达式"
<variable name="user" type="com.example.User"/>
2.1.3 在布局文件中属性值里使用 “@{}” 的语法,来表示"databinding表达式"。结合2.2,这里 TextView 的文本被设置为 user 中的 firstName 属性。其中user.firstName
和 java 中的表达式含义类似。
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
2.2. 数据对象
刚刚在布局文件中我们使用com.example.User
类定义了一个 user 变量,现在我们假设 User 类是一个 plain-old Java object(POJO)。
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
因为成员变量都是final的,所以上面这个User类型的对象拥有不可改变的数据(immutable)。在应用中,这种写一次之后永不变动数据的对象很常见。
这里也可以使用 JavaBeans 类:
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;
}
}
从 data binding 的角度看,这两个类是等价的。TextView 的android:text属性的表达式@{user.firstName},对于 POJO 对象这个表达式会读取 firstName 字段的值,对于 JavaBeans 对象会调用 getFirstName() 方法。此外,如果 user 中有 firstName() 方法存在,@{user.firstName}表达式也可以表示对firstName() 方法的调用。
2.3. 数据绑定
上面工作完成后,数据绑定工具在编译时会基于布局文件生成一个 Binding 类。默认情况下,这个类的名字是基于布局文件的名字产生的,先把布局文件的名字转换成帕斯卡命名形式,然后在名字后面接上”Binding”。例如,上面的那个布局文件叫 main_activity.xml,所以会生成一个 MainActivityBinding 类。这个类中包含了布局文件中所有的绑定关系,并且会根据绑定表达式给布局文件中的 View 属性赋值(user变量和user表达式,view绑定,view数据绑定,view命令绑定)。编译时产生Binding类主要完成了2个事情,1.解析layout文件,根据data标签定义成员变量;2.解析layout文件,根据"databinding表达式"产生绑定代码。Binding 创建好之后还需要,创建 Binding 类的对象,并和view绑定。
2.3.1 在 Activity inflate 一个布局的时候创建 binding,例子如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
就这么简单!运行应用,你会发现测试信息已经显示在界面中了。
DataBindingUtil.setContentView(this, R.layout.main_activity);
这句代码主要做了3件事情,1.把布局设置给Activity,填充为view树;2.创建Binding类对象;3.把view保存在Binding类的成员中,绑定view。这种方式只适合用于Activity中。
2.3.2 也可以通过下面这种方式绑定view:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
MainActivityBinding.inflate() 会填充 MainActivityBinding 对应的布局,并创建 MainActivityBinding 对象,把布局和MainActivityBinding对象绑定起来。
2.3.3 如果在 ListView 或者 RecyclerView 的 adapter 中使用 data binding,可以这样写:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
2.4. 命令绑定
上面演示了数据绑定,下面演示命令绑定,databinding 还允许你编写表达式来处理view分发的事件(比如 onClick)。事件属性名字取决于监听器方法名字,例如View.OnLongClickListener有onLongClick()的方法,因此这个事件的属性是android:onLongClick。
处理事件有两种方法:
方法绑定:在您的表达式中,您可以引用符合监听器方法签名的方法。当表达式的值为方法引用时,Data Binding会创建一个监听器,并封装方法引用和方法所有者对象,然后在目标视图上设置该监听器。如果表达式的值为null,DataBinding 则不会创建侦听器,而是设置一个空侦听器。
Lisenter 绑定:如果事件处理表达式中包含lambda表达式。DataBinding 会创建一个监听器,设置给视图。当事件分发时,侦听器才会计算lambda表达式的值。
2.4.1. 方法绑定
事件可以直接绑定到事件处理器的方法上,类似于android:onClick可以分配一个 Activity 中的方法。与View#onClick属性相比,方法绑定的主要优点是 DataBinding 表达式在编译时就执行过了,因此如果该方法不存在或其签名不正确,您会收到一个编译时错误。
方法绑定和监听器绑定之间的主要区别是,包裹方法引用的监听器实现是在数据绑定时创建的,监听器绑定是在触发事件时创建的。如果您喜欢在事件发生时执行表达式,则应使用监听器绑定。
如果想要将事件处理直接分配给处理程序,那就使用方法绑定表达式,该表达式值是要调用的方法名称。例如,数据对象有如下方法:
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.Handlers"/>
<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>
注意,@{handlers::onClickFriend}表达式中onClickFriend的方法签名必须与android:onClick监听器对象中的方法签名完全匹配。
2.4.2. Lisenter 绑定
Lisenter 绑定是在程序运行中事件发生时才绑定表达式。它和方法绑定类似,但 Listener 绑定允许运行时绑定的任意的数据表达式。此功能适用于版本2.0及更高版本的Android Gradle插件。 在方法绑定中,方法的参数必须与事件侦听器的参数匹配。在 Listener 绑定中,只要返回值与 Lisenter 预期的返回值匹配就行(除非它期望void)。
2.4.2.1 例如,您有一个 presenter 类,它具有以下方法:
public class Presenter {
public void onSaveClick(Task task){}
}
然后,通过lambda表达式您可以将点击事件绑定到您的类中,如下所示:
<?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>
侦听器只允许 DataBinding 表达式的根元素是lambda表达式。当在表达式中使用回调时,数据绑定自动创建必要的侦听器并且为事件注册。当视图触发事件时,数据绑定才执行给定的表达式。与在正则绑定表达式中一样,在执行这些侦听器表达式时,DataBinding 已经做好了空值和线程安全性的处理。
2.4.2.2 在上面的示例中,我们没有在lambda表达式中定义传递给 onClick(android.view.View)方法的视图参数。侦听器绑定为侦听器参数提供两个选择:1.忽略方法的所有参数;2.命名所有参数。
如果您喜欢命名参数,可以在表达式中使用它们。例如,上面的表达式可以写成:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
如果你想要使用表达式中的参数,可以在这样使用:
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
您也可以使用具有多个参数的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)}"
如果由于空对象而无法计算表达式,数据绑定将返回该类型的默认Java值。例如,引用类型为null,int为0,boolean为false等。
2.4.2.3 如果需要使用带谓词(例如三元)的表达式,则可以使用void作为符号。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
总结起来方法绑定和Listener绑定的区别如下:
- 方法引用绑定不能是表达式,Lisenter 绑定可以是表达式;
- 方法引用绑定在绑定的时候会执行 DataBinding 表达式,可以自动处理空指针问题,Listener 绑定在事件触发的时候才会执行 lambda 表达式;
- 方法引用绑定会限制绑定的方法参数列表,返回值必须和监听器中的方法一致,Listener 绑定只限制 lambda 表达式中语句的返回值和监听器中的方法一致;
方法引用不止能在 android:onClick= 这种命令绑定属性中使用,在其他数据绑定属性中也可以使用,而且可以使用表达式作为参数。
<TextView android:id="@+id/context_demo" android:text="@{user.load(context, @id/context_demo)}" />
public String load(Context context, int field) { return context.getResources().getString(R.string.app_name); }
事件处理除了上述两种方法,还可以直接以数据绑定的形式绑定一个监听器对象(属性setter小节讲解)。
2.4.3. 避免复杂监听器
Listener表达式非常强大,可以使代码非常容易阅读。另一方面,包含复杂表达式的 Listener 又会使布局难以阅读和难以维护。这些表达式应该保持简单,比如只用来从UI传递可用数据到回调方法一样简单,任何业务逻辑还是应该在从侦听器表达式调用的回调方法中实现。
为了避免冲突,有些点击事件处理程序他们需要一个专门的属性,它们不是android:onClick。databinding已通过@BindingMethods注解(属性setter小节讲解)创建以下属性来避免此类冲突。
Class | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |