这种表达式语言(expression language
)使我们可以使用表达式处理 view 的事件。Data Binding
库会自动生成绑定类(binding class
)用来处理 view 和 data 的绑定关系。
使用 Data Binding
的布局文件和传统的布局文件稍有不同,它的根标签是 layout
,里面会有一个 data
子标签和一个根 view 子标签。这个根 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
变量,将会在绑定表达式中用到。
<variable name="user" type="com.example.User" />
复制代码
绑定表达式用于为属性赋值,它使用的语法是 @{}
。在下面的例子中,TextView
控件的属性 text
,被赋值为 user
变量的 firstName
属性值:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
复制代码
数据源
假设现在有一个描述 User
实体的数据对象:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
复制代码
这个数据类的成员属性都是不可变的、是 public 的。它还有另外一种写法,成员属性是 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;
}
}
复制代码
从数据绑定的角度来看,上面两个实体类是等价的。用于给 android:text
属性赋值的表达式 @{user.firstName}
,会自动读取前一个类的 firstName
属性,或者调用后一个类的 getFirstName()
方法。而且,如果 firstName()
方法存在,也会调用这个方法。
可绑定的数据(Binding data
)
每个布局文件都会生成一个对应的绑定类。默认的绑定类的名字是文件名转为驼峰写法并加上后缀 Binding。比如文件名是 activity_main.xml
,对应的绑定类名为 ActivityMainBinding
。这个绑定类保存了数据变量和 view 属性的绑定关系,并且知道如何为 view 属性赋值。推荐的方式是在加载布局的时候创建绑定类,如下所示:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Test", "User")
}
复制代码
另外,还可以使用 inflate()
方法创建绑定类:
val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())
复制代码
如果是在 Fragment
、 ListView
、 RecyclerView
中使用 Data Binding
,你可能更倾向于使用 inflate()
方法或 DataBindingUtil
类,如下所示:
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
复制代码
表达式语言(expression language
)
1 常用特性
表达式语言看起来很像代码里面的表达式。你可以在表达式中使用如下的操作符和关键字:
- 计算相关运算符
+ - / * %
- 字符连接符
+
- 逻辑运算符
&& ||
- 二元运算符
& | ^
- 一元运算符
+ - ! ~
- 移位运算符
>> >>> <<
- 比较运算符
== > < >= <=
(注:<
运算符需要写成<
) instance of
()
- 字符、字符串、数字、null
- 强转
- 方法调用
- 属性访问
- 数组访问
[]
- 三元元素符
?:
举例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
复制代码
2 不可用操作
下面操作只能在代码里使用,不能用于表达式语法:
this
super
new
- 泛型声明
3 空结合运算符(??
)
空结合运算符左边表达式如果不为空,使用左边表达式结果,否则使用右边表达式结果
android:text="@{user.displayName ?? user.lastName}"
复制代码
它等价于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
复制代码
4 属性引用
使用绑定表达式,可以引用一个类的成员变量、get 方法、ObservableField
:
android:text="@{user.lastName}"
复制代码
5 避免空指针异常
生成的绑定类会自动判空从而避免空指针异常。比如,表达式 @{user.name}
,如果 user
是空,user.name
会被赋予默认值 null
。如果引用的是 user.age
,age
的类型是 int
,那么会使用 0
作为默认值。
6 集合
常用的集合,如数组、list、sparse list、map 等,可以方便地使用 []
操作符访问它们的元素。
<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 中的元素,除了可以使用 @{map[key]}
,也可以使用 @{map.key}
。
7 字符串的写法
可以使用单引号包裹属性,在表达式中使用双引号,如下所示:
android:text='@{map["firstName"]}'
复制代码
也可以使用双引号包裹属性,在表达式中使用单引号,如下所示:
android:text="@{map[`firstName`]}"
复制代码
8 访问资源
可以在表达式中使用如下语法访问资源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
复制代码
9 事件处理
Data Binding
可以让我们在表达式中处理 view 分发的事件,比如 onClick()
。属性的名字取决于监听器方法的名字,比如,View.OnClickListener
有一个 onClick()
方法,所以对应的属性名就是 android:onClick
。
处理 view 事件有两种方法:
- 方法引用:在绑定表达式中,可以引用一个方法处理事件,需要注意的是,这个方法需要和监听器的方法签名完全一致。如果绑定表达式的结果是
null
,对应的 view 不会设置监听器。 - 监听器绑定(
listener binding
):这种方式总是会给对应的 view 设置监听器,当 view 收到事件时会调用 lambda 表达式。
- 方法引用
事件可以和方法直接绑定,这种方式与
android:onClick
可以和 activity 中一个方法绑定很类似。这种方式的优势是,绑定表达式是在编译期间处理的,如果方法不存在或者签名不匹配,会直接报错。和监听器绑定不同的是,方法引用的方式会在数据绑定的时候创建监听器,监听器绑定则是在收到事件的时候创建监听器。
示例如下:
class MyHandlers {
fun 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>
复制代码
注: 表达式中的方法签名必须与监听器中的方法签名一致。
- 监听器绑定(
listener binding
)监听器绑定的方式,只有在收到事件的时候才会执行。它和方法引用很像,但是它可以使用任意的表达式。这个特性在 Gradle 2.0 及以后可以使用。
方法引用的方式,要求方法的签名必须和监听器的方法签名一致。但是监听器绑定的方式,只要求返回值一致即可。例如,假如下面的 Presenter 类有一个
onSaveClick()
方法:
class Presenter {
fun onSaveClick(task: Task){}
}
复制代码
onSaveClick()
可以与android:onClick
绑定,如下所示:
<?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>
复制代码
在上面这个例子中,我们没有定义 view 参数。监听器绑定提供了两种选择:要么忽略所有参数,要么显式写出所有参数。如果你喜欢写出参数,如下所示:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
复制代码
如果你需要使用这些参数,如下所示:
class Presenter {
fun onSaveClick(view: View, task: Task){}
}
复制代码
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
复制代码
同时,也可以有多个参数,如下所示:
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
复制代码
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
复制代码
此外,如果你监听的事件返回值不是
void
,那么你的表达式也需要返回相同的返回值。如下所示:
class Presenter {
fun onLongClick(view: View, task: Task): Boolean { }
}
复制代码
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
复制代码
如果表达式不能正常执行,那么会返回默认值,引用类型返回 null,int 类型返回 0,布尔类型返回 false。
Imports、variables、includes
Data Binding
提供了诸如 imports
、 variables
、 includes
等特性。imports
用于导入所需要的类,方便引用;variable
用于定义一个变量,方便在绑定表达式中使用。includes
使我们可以复用布局。
- Imposts
下面这个例子展示了导入 View
这个类到布局文件中:
<data>
<import type="android.view.View"/>
</data>
复制代码
导入的目的就是为了方便在绑定表达式中使用。下面的例子展示了在表达式中引用 View
类的两个常量 VISIBLE
和 GOEN
:
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
复制代码
-
- 类型别名
如果导入的两个类名字相同,可以为其中一个或两个起别名以区分:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
复制代码
注:
java.lang.*
下面的类会自动导入。
- Variables
变量的声明用于在绑定表达式中使用,如下所示,声明了 user
、 image
、 note
三个变量:
<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>
复制代码
自动生成的绑定类,包含了这些变量的 get 和 set 方法。这些变量都会有默认值,引用类型默认值是 null
,int 类型默认值是 0,布尔类型默认值是 false。
- Includes
变量可以传递给 include
布局,如下所示,user
变量传递给了 name.xml
和 contact.xml
两个布局:
<?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
不支持 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>
复制代码