Data Binding Library官网文档翻译 2 -布局
这一篇是整个系列的第二篇翻译,像上一篇一样,翻译不准确的地方欢迎大家批评指正。同样附上官网链接。同时,转载请注明出处,https://www.jianshu.com/p/df795c781e50
1. 前言
你可以使用表达式来处理控件分发过来的事件。Data binding库可以自动生成binding class,来绑定布局中的控件和数据类。
使用了Data binding的布局文件内容会和普通的布局文件稍有不同。主要是我们在需要设置layout作为根标签(root tag),里面包含一个data标签和一个正常的view标签。举例如下:
<?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标签中的user变量可以在layout文件中使用。如图,databinding表达式需要使用"@{}"语法,上面代码中的TextView的text被设置成了user变量的firstName和lastName属性。
注意,Databinding表达式应该保持短小精悍,因为他们无法集成测试而且IDE的支持有限。遇到复杂的表达式,我们可以使用自定义binding适配器来解决custom binding adapters
2. 数据对象
我们现在假设您有一个普通的对象来描述User实体。这种对象的数据永远不会改变(final)。在应用程序中它们只会被读一次之后永远不再改变。注意这里的firstName和lastName都是public的。
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
当然也可以使用遵循一组约定的对象,例如get,set方法,注意这里的firstName和lastName都是private的,如以下示例所示:
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的角度来看,这两个类是等价的。用于android:text属性的表达式@ {user.firstName},在前一类中直接访问firstName字段,而后一类中调用getFirstName()方法,来获取firstName的内容。或者,如果firstName()方法存在也会使用这个方法。
2. Binding data
每个layout文件都会生成一个对应的binding类。默认情况下,这个binding class的文件名是基于layout文件名的。将layout文件名按照Pascal命名规则转换,再添加一个suffix后缀即可。例如activity_main.xml文件的对应binding class的名字就叫ActivityMainBinding。
binding class包含了layout中的属性(比如上面的user变量)以及布局的各种View,并且能为binding表达式赋值。(即通过binding class可以访问到layout中的data和各个view)。我们建议在inflate布局的时候创建bindings。一般Activity中可以这样使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
上面的代码在运行的时候会在UI中展示“Test”和“User”。除此之外也可以用下面的方法获取View:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果您在Fragment,ListView或RecyclerView适配器中使用数据绑定项,您可能更喜欢使用binding class(binding类的父类)或DataBindingUtil类的inflate()方法,如以下代码示例所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
3. 表达式语言
3.1 Common features
表达式语言和managed code中的表达式很类似。你可以在表达式语言中使用以下操作符和关键字:
- Mathematical + - / * %
- String concatenation +
- Logical && ||
- Binary & | ^
- Unary + - ! ~
- Shift >> >>> <<
- Comparison == > < >= <=
- instanceof
- Grouping ()
- Literals - character, String, numeric, null
- Cast
- Method calls
- Field access
- Array access []
- Ternary operator ?:
例如
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
3.2 Missing operations
以下的操作符无法在表达式语言中使用
- this
- super
- new
- Explicit generic invocation(显式通用调用)
3.3 Null coalescing operator 空结合运算符
A ?? B,这个运算符会在左边不为null的时候使用左边,否则使用右边
android:text="@{user.displayName ?? user.lastName}"
// 这两个表达式效果相同
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
3.4 属性引用
表达式可以通过下面的格式来引用class中的属性,对于fields,getters和ObservableField对象使用方法都是一样的(这一点很重要,可以回去重看 2.数据对象)。
android:text="@{user.lastName}"
3.5 避免空指针错误
生成的数据绑定代码会自动检查空值并避免空指针异常。例如,在表达式@ {user.name}中,如果user为null,则为user.name分配其默认值null。如果引用user.age,其中age的类型为int,则数据绑定使用默认值0。
3.6 集合Collections
为方便起见,可以使用[]运算符访问常见的各种集合,例如数组arrays,列表list,稀疏列表sparse lists和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]}"
注意,针对map。还可以使用object.key表示法引用map中的value。例如,上面示例中的@ {map [key]}可以替换为@ {map.key}。
3.7 字符串文字
您可以使用单引号括起属性值,然后再表达式中使用双引号,双引号括起来的会被当做是string字符串。如以下示例所示
android:text='@{map["firstName"]}'
当然,用双引号包裹属性值,表达式中使用单引号也是可以的。
android:text="@{map[`firstName`]}"
3.8 资源文件
使用如下语法即可在表达式用引入资源文件
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
提供相关参数之后也可以使用Format strings and plurals功能。(格式化string我们经常用,可以动态替换string字符串中的某些文字。format plurals主要是针对英文中单复数时文字本身会有变化而生成的功能)
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当复数采用多个参数时,应传递所有参数:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
有些资源需要明确指明类型,如下表中:

4. 事件处理Event handling
Data binding允许你编写表达式来处理views分发来的事件,比如onClick方法。事件的属性名称往往由listener中的方法名称来确定,但也有一些例外。比如,View.OnClickListener有onClick方法,所以这个时间的对应属性名就叫做 android:onClick。
接下来解释一下上面说到的例外情况。因为存在一些事件处理方法,它们的方法名也是onClick,但是为了避免冲突,就设置了别的属性。其实原因是setXXXlistener时传入了OnClickListener,没有针对自身额外写一个新的Listener。如下:
Class | Listener setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener((View.OnClickListener)) | android:onZoomOut |
上面说了一些注意事项,现在来具体看一下。我们有如下两种机制来处理事件。
-
Method references(方法引用): 在表达式中,您可以遵循Listener中的方法的签名,来引用这个方法。当表达式为Method references时,Data binding将方法引用和所有者对象(即传入到onClick中的View参数)一起包装在listener中,并在目标视图上设置该侦听器。如果表达式求值为null,则数据绑定不会创建Listener,而改为设置空侦听器(即setOnClickListener)。
-
Listener bindings:事件发生时,会计算我们设置的lambda表达式(即这个方法是将属性设置为一个lambda表达式)。给View设置之后,每次事件被分发过来,Data binding都会创建一个listener,然后计算lambda表达式。
4.1 Method references
事件可以直接绑定到处理程序方法,类似于android:onClick中可以写一个对应的Activity中的定义的方法。就是在Activity中写一个比如onTestClick方法,然后设置android:onClick="onTextClick"。下面举个例子:
// XML里的代码
<TextView
android:id="@+id/tv"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#ffcc00"
android:clickable="true"//设置可以点击,TextView默认是不能点击的
android:onClick="showMsg"//设置点击的方法名
android:text="设置单击事件"
android:singleLine="true"
android:tag="hello" />
// 在Activity中设置单击方法,方法名要和XML中的一样
public void showMsg(View view){
//必须写参数,View是事件源,回调的时候传入事件源
Toast.makeText(this, tv.getTag().toString(),Toast.LENGTH_LONG).show();
}
与上面代码里的方案相比,Method references主要优点是表达式在编译时处理,因此如果该方法不存在或其签名不正确,则会收到编译时错误。
要将事件分配给他的handler处理者,我们需要正确的使用binding表达式,对应属性里填入要调用的方法名称。比如:
// 处理者,包裹了处理方法
public class MyHandlers {
// 处理方法,注意参数是View
public void onClickFriend(View view) { ... }
}
// XML代码
// 如下代码,会将view的clickListener设置为onClickFriend方法
<?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>
注意,表达式中调用的方法的签名,要跟对应的listener中的方法的签名相同。比如android:onClick对应OnClickListener的onClick(View view)方法,所以handlers::onClickFriend方法在定义的时候也是传进对应的View参数。
4.2 Listener bindings
Listener bindings(侦听器绑定)是在事件发生时运行的绑定表达式。它们类似于方法引用,但你可以使用更灵活的binding表达式。Android Gradle Plugin for Gradle 2.0 以上版本提供此功能。
使用method references时,method的参数必须与对应的事件监听器的参数保持一致。而在listener bindings中,你只需要保持返回值与事件监听器的预期返回值一致,除非它期望返回值是void。下面给一个例子~
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>
在表达式中使用回调时,data binding会自动创建必要的Listener并为事件注册它。当View触发事件时,Data binding会计算给定的表达式。与常规绑定表达式一样,在计算这些侦听器表达式时,我们只能拿到空的表达式,并且保持线程安全。
4.2.1 lambda表达式中参数的定义
上面的例子中,我们没有定义传递给onClick方法的view参数。listener bindings为监听器参数提供了两种选择。第一种,可以选择忽略所有参数。第二种,命名所有参数(注意,要么都命名,要么一个都不命名)。 例如,上面的表达式可以写成如下:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
如果你需要在表达式中调用监听器的参数,那么就需要命名他们。:
public class Presenter {
// 增加一个View参数
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
如果参数超过一个:
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)}" />
4.2.2 Listener bindings的返回值问题
需要注意的是,如果监听的时间返回值不是void,那么我们定义的表达式就必须返回相同类型的值。比如我们想要监听long click event。我们应该向下面这样写
public class Presenter {
// 注意这里不是 void 而是 boolean
public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
// longclick 注意onLongClick返回值为boolean
setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return false;
}
});
// longclick 注意onClick返回值为void
setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
注意表达式应该尽量简洁,复杂的逻辑应该放到监听器表达式的回调方法中执行。
4.3 两者的区别
Method references和Listener bindings之间的主要区别在于
-
前者是在绑定数据时创建的,而不是在触发事件时创建的。如果您希望在事件发生时执行代码,则应使用后者。
-
前者需要方法的签名(即形参和返回值)与对应listener一致。而后者只要求保持返回值一样就可以(如果返回值是void,那么返回值也可以不同)。
个人理解,用代码来说大致是这个意思:
// Method references
android:onClick="@{handlers::onClickFriend}"
// 效果相当于写了一个onClick方法
tv.setOnClickListener(new View.OnClickListener() {
// onClickFriend与onClick等价
// 所以使用Method References时两者参数要保持一致
handler::onClickFriend(v);
});
// Listener bindings
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
// 这样设置相当于新写了一个ClickListener
tv.setOnClickListener(new OnXXXClickListener(){
// 可以传入onClick中的参数
public void onXXXClick(View theView, Task task) {
// task是xml里设置的
presenter.onSaveClick(theView, task);
}
});
// lambda公式与下面等价
new OnXXXClickListener(){
// 可以传入onClick中的参数
public void onXXXClick(View theView) {
// task是xml里设置的
presenter.onSaveClick(theView, task);
}
}
4.4 注意事项 - 避免listener过于复杂
监听器表达式非常强大,可以使您的代码非常容易阅读。另一方面,如果Listeners包含复杂表达式,也会使您的布局难以阅读和维护。这些表达式应该像将UI中的可用数据传递给回调方法一样简单。复杂的业务逻辑应该在回调方法内部进行实现,而不是卸载listener 表达式里。
5. Imports, variables, and includes的用法
数据绑定库提供imports, variables, 以及 includes等功能。这些也都是最常用的功能。
import使布局文件中更容易引用别的class。variables可以在绑定表达式中访问类的属性。include可以帮助你复用复杂的布局。
5.1 Imports
Imports可以让你在layout文件里引用class,就像在managed code中一样。data标签中可以导入多个import。以下代码示例,将View类导入布局文件:
<data>
<import type="android.view.View"/>
</data>
// import view类之后,就可以在binding表达式中引用它了
// 下面展示了引用View的常量,VISIBLE和GONE
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
5.1.1 Type aliases
当两个类的类名冲突时,其中一个类需要用一个alias别名来重命名(两个完整路径不同的类,类名是有相同的可能性的)。主要是为了区分。
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
设置之后,我们就可以使用Vista来引用com.example.real.estate.View类,而View类就是来引用android.view.View类。
5.1.2 Import other classes
- 导入的类型可以用作变量和表达式中的类型引用。以下示例展示了用User和List作为变量类型:
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
注意:Android Studio尚未处理导入时,导入变量的自动完成功能可能无法在IDE中运行。你可以在定义variable的时候使用完整路径名,来变异和运行程序。
- 我们也可以使用import进来的类型来强转表达式中的某一部分。以下示例将connection属性强制转换为User类型。
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
- 表达式中也可以使用imported类型来引用static变量和static方法。下面的代码中import了MyStringUtils类,并且引用了capotalize方法。
<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"/>
就像managed code里一样,java.lang.*会自动被import,可以直接使用。
5.2 Variables
您可以在data元素(element)中使用多个variable元素。每个variable元素作为一个属性被设置到layout文件里,然后可以在binding表达式里使用。以下示例声明了user,image,note三个variable:
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
在编译时检查变量类型,因此如果变量实现Observable接口或是Observable collection,那么应该反映在类型中。如果变量是未实现Observable接口的基类或接口,则不会观察变量。
当存在用于各种配置的不同布局文件(例如,横向或纵向)时,variables变量会被合并在一起。这些layout文件之间必须保证不存在冲突的变量定义。
自动生成的binding class中针对每个variable都有setter和getter方法。variable在调用对用的setter方法设置之前,都是使用默认值。引用类型就设为null,int型就是0,boolean就是false,等等。
此外,binding class还会生成还有一个很特殊的context variable,可以在binding表达式里使用。context的值是来自顶层view的getContext方法的Context类对象。context变量会被对应名称的现实变量声明给覆盖掉。
5.3 Includes
通过使用app命名空间和定义的variable名,variables可以从自身所在的布局,被传递到使用include引入的布局绑定中。下面的例子展示了name.xml和contact.xml文件使用user变量:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>
注意,data binding不支持把include的布局作为merge元素中的直接的孩子节点。比如下面的布局代码就是不支持的:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<merge><!-- Doesn't work -->
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>