框架组件(二)Data Binding 2-布局与绑定表达式
该系列文章是对Android推出的架构组件相关文章,按作者自己理解来翻译的,同时标记有作者自己一些简单笔记。如果读者发现文中有翻译不准确的地方,或者理解错误的地方,请不吝指教。
源自Google官方
Data Binding Library 一文的翻译与归纳
其他相关链接:
Android Jetpack Components
[TOC]
表达式语言允许你使用表达式处理View调度的方法。Data Binding 库自动生成将布局中view与数据对象绑定的类。
Data Binding 布局文件与普通布局略有不同,根布局使用 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>
data
中的 user
variable 描述了该布局可以使用的一个属性
<variable name="user" type="com.example.User" />
布局中的表达式使用@{}
语法来定义属性值。 TextView
设置为 user
变量的 firstName
属性。
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
Note: 布局表达式需要保持短小简介,因为他们不能进行单元测试,且IDE支持功能也有限。你可以使用自定义 binding adapter 来简化布局表达式。
数据对象
假设我们现在有一个传统的 User
实体对象类:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
这种类型对象通常用于读取一次之后不会再改变的数据,也可能会遵循一些约定,比如java中的访问器方法。如下示例所示。
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;
}
}
从数据绑定的角度来看,上面两个类是等价的。@{user.firstName}
表达式会使用前者的 firstName
属性和后者的 getFirstName()
方法给 android:text
属性赋值。另外,如果存在 firstName()
方法,它也可以正常解析。
绑定数据
每个布局文件都会生成一个绑定类。默认情况下,生成的类基于布局名称使用驼峰命名并添加Binding后缀来命名。之前的布局文件名称是 activity_main.xml
,所以对应生成类是ActivityMainBinding
。该类包含所有从布局data属性到布局view的所有绑定,并知道如何为绑定表达式赋值。推荐在布局引入创建绑定,以下是实例:
@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);
}
在运行app时,将会在界面中显示 Test 用户。另外,你可以使用 LayoutInflater
获取视图,如下所示:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
如果在 Fragment
,ListView
或者 RecyclerView
适配器中使用数据绑定,你可能更喜欢使用绑定类或者 DataBindingUtl
的 inflate()
方法,如下所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
表达式语言
常见特征
表达式语言看起来很像是代码中的表达式。你可以在表达式中使用以下运算符和关键字:
- 数学运算符
+ - / * %
- 字符串拼接
+
- 逻辑运算符
&& ||
- 二进制
& | ^
- 一元
+ - ! ~
- 位移
>> >>> <<
- 比较
== > < >= <=
instanceof
- 括号
()
- 字符类型 - 字符、字符串、数字、null
- 强制转换
- 方法调用
- 变量访问
- 数组访问
[]
- 三目运算符
? :
例子:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
缺少的操作符
以下是可以再代码中使用但是表达式不支持的操作符:
this
super
new
- 显示通用调用
Null合并 操作符
??
操作符会在左侧不为空时选择前者,左侧为空时选择右侧。
android:text="@{user.displayName ?? user.lastName}"
该操作等价于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
一个表达式可以使用以下格式引用类属性,对于 fields
、getters
和 ObservableField
都是一样的格式:
android:text="@{user.lastName}"
避免空指针异常
生成的数据绑定代码会自动检测 null
值,避免了空指针异常。举个例子,在表达式 @{user.name}
中,如果 user
为空,user.name
将默认分配为 null
值。如果你引用的是 user.age
,其中age是 int
类型,那么数据绑定时将默认使用0。
集合
常见集合,例如 array、list、SparseArray、与 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]}"
注意: 你也可以使用
.
符号来获取 map 中的元素。举个例子,在上面例子中的@{map[key]}
可以替换为@{map.key}
。
字符串文本
你可以使用单引号将属性值括起来,这将允许你在表达式中使用双引号来表示字符串:
android:text='@{map["firstName"]}'
当然也可以用双引号将属性值括起来。这时字符串文本使用单引号来表示:
android:text="@{map[`firstName`]}"
资源
你可以用以下表达式语法来分配resource:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
可以通过提供参数来格式化字符串或复数:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当复数采用多个参数时,应该传递所有参数:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
一些资源需要指定明确的类型,如下所示:
类型 | 正常引用 | 表达式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
事件处理
Data binding 允许你使用表达式来处理 view 分发的时间(例如 onClick
方法)。时间属性名称由 Listener 方法来决定,但有一些例外。比如 View.OnClickListener
有一个 onClick()
方法,所以该事件对应属性值是 android:onClick
。
这里有一些特殊的点击事件处理需要用 android:onClick
之外以属性避免冲突。如下所示:
类 | 设置监听 | 属性 |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
你可以用以下机制处理事件:
-
方法引用:在你的表达式中,你可以引用符合监听方法规定的方法。当表达式判定是一个方法时,Data binding 会将方法和方法所有者对象包装到一个 listener 中,同时将 listener 设置到指定 view 里。如果表达式判定为
null
,Data binding 会给 view 设置监听为null
。 -
listener绑定:该方法是在事件发生时计算
lambada
表达式。 Data binding 总是会创建 listener 并设置给view,当事件分发时,创建的监听器计算lambada
表达式。
方法引用
事件可以直接绑定到方法上,同样的,android:onClick
可以关联 activity的一个方法。和 View 的 onClick 属性相比,一个主要的优点是表达式是在编译时处理,所以如果方法不存在或者格式不对,会直接提示编译错误。
方法引用和listener绑定主要不同点是,当数据绑定时才创建实际的 listener 实例,而不是触发事件时。如果你希望事件触发时计算表达式,你应该使用 listener 绑定。
要将事件分配给处理者,需要使用不同绑定表达式,即值设置为要调用的方法名称。如下示例:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
如下设置,绑定表达式可以将 view 单击监听分配给 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 重写的方法格式、参数完全一致。
Listener 绑定
Listener 绑定是在事件发生时运行绑定表达式。和方法引用十分类似,但是它们可以让你运行任意数据绑定表达式。这个功能只适用于Gradle版本2.0及更高版本。
在方法引用中,指定方法的参数必须与事件 listener 方法参数匹配。在 listener 绑定中,只需要你的返回值与 listener 返回值匹配(或者返回值是void)。举个例子,思考下面 有 onSaveClick
方法的 presenter 类。
public class Presenter {
public void onSaveClick(Task task){}
}
然后你可以像下面这样绑定 click 事件到 onSaveClick()
方法上:
<?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>
在表达式中使用 callback 时,data binding 自动为事件创建必要的 listener。当 view 触发事件时,data binding 计算对应的表达式。与常规表达式一样,在计算这些监听表达式时,代码依然会判空且是线程安全的。
在前面的例子中,我们还没有定义过给 onClick
传递 view
参数。对于监听参数 Listener 绑定有两种选择:你可以忽略方法的所有参数或声明所有参数。如果你更喜欢声明参数,你可以在表达式中这样使用:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
或者你希望使用表达式中的view参数,可以这样做:
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
你可以使用有多个参数的 lambada 表达式:
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
,你的表达式也必须返回相同类型的值。比如,如果你监听的是 long click 事件,你的表达式必须返回一个 boolean 值。
public class Presenter {
public boolean onLongClick(View view, Task task) { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于 null 对象导致表达式无法计算,data binding 会返还对应类型的默认值。比如对象引用对应 null
,int
对应 0
, boolean
值对应 false
等等。
如果你的表达式有?:
或??
之类的计算,你可以使用 void
来作为一个元素。如下所示:
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂监听
监听表达式很强大,且可以让你的代码更简单易懂。但另一方面,如果监听表达式复杂会让你的 layout 更难理解和维护。所以这些表单式应该仅仅用于将数据从 UI 传递到你的回调方法。你应该在回调方法中来实现业务逻辑,而不应该在监听表达式中实现。
import,variable 和 include
Data Binding 库提供了 import
、variable
和 include
这些功能。import
让你的布局文件更轻松的引用类;variable
允许你定义能在绑定表达式中使用的属性;include
让你可以重用复杂的布局。
imports
Import 允许你在布局文件中轻松引用类,就像在代码里一样。data
元素中可以定义0个或多个 import
元素。如下示例展示在布局文件中引入 View
类:
<data>
<import type="android.view.View"/>
</data>
引入 View
类后允许你在绑定表达式中使用它。下面例子展示了如何使用 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}"/>
类型别名
当多各类名称冲突时,其中某些类可以定义一个别名。下面例子展示了将在 com.example.real.estate
包中的 View
类重命名为 Vista
。
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
这样在布局文件中就可以用 Vista
表示 com.example.real.estate.View
类,而 View
表示 android.view.View
。
import 其它类
引入类型可以用作变量或者表达式中的类型引用。下面例子展示了 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>
你也可以在表达式中使用引入类型强制转换。就像下面的例子一样:
<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"/>
就像代码一样,java.lang.*
会被自动引入
Variable
你可以在 data
元素中使用多个 variable
元素。每个 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>
这些 variable 类型会在编译时被检测,因此如果 variable 实现了 Observable 或者 observable collection,必须在类型中体现。如果 variable 是没有实现 Observable
接口的基类或接口,将不能被观测。
当存在不同配置下的布局文件时(比如横屏、竖屏),variable 将被组合。这些布局文件之间不能存在冲突的变量定义。
生成的 binding 类中每个 variable 都有 get
和 set
方法。在给这些 variable 设置值之前,都会使用对应类型默认值。比如引用类型为null,int
为0,boolean
为 false
等等。
一个特殊的 variable 名称是 context
,为需要的表达式生成,该变量是由根 view 的 getContext()
方法获取的 Context
对象。如果要覆盖 context
变量,则需要显示声明 variable 来覆盖。
Includes
variable 可以用通过 includ 标签的 bind
属性传递到引入的布局中。下面例子展示了 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>