Android Jetpack架构篇:Data Binding(一)
<spane id="layouts_binding_exp">Layouts and binding expressions(布局和绑定表达式)</span>
该表达式语言,允许你通过编写表达式来处理视图的事件分发。数据绑定库(Data Binding Library)自动生成将布局中的视图与data对象绑定所需的类。
数据绑定布局文件略有不同,必须以 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文本设置为user变量的 firstName属性:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
注意:布局表达式应保持短小且简单,因为它们不能进行单元测试并且IDE支持有限。为了简化布局表达式,您可以使用自定义绑定适配器。
Data object(数据对象)
假设有一个普通的实体对象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 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}用于该android:text 属性的表达式访问firstName前一类中的字段和getFirstName()后一类中的 方法。或者,firstName()如果存在该方法,也可以解决。
Binding data(绑定数据)
为每个布局文件生成绑定类。默认情况下,类的名称基于布局文件的名称,将其转换为Pascal大小写并向其添加Binding后缀。上面的布局文件名是 activity_main.xml相应生成的类 MainActivityBinding。此类包含布局属性(例如,user变量)到布局视图的所有绑定,并知道如何为绑定表达式指定值。建议绑定的推荐方法是在扩展布局时执行此操作,如图所示在以下示例中:
@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);
}
注意:上面MainActivityBinding是官网中写法,正确写法是ActivityMainBinding请读者自行替换,下同。
在运行时,应用程序在UI中显示Test用户。或者,您可以使用a获取视图LayoutInflater,如以下示例所示:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果您在 Fragment, ListView或RecyclerView适配器中使用数据绑定,则可能更适合使用 inflate() 绑定类或 DataBindingUtil类的方法,如以下代码示例所示:
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 前者,则选择右操作数null。
android:text="@{user.displayName ?? user.lastName}"
这在功能上等价于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
表达式可以使用以下格式引用类中的属性,这对于字段,getter和ObservableField 对象是相同的 :
android:text="@{user.lastName}"
空指针异常避免
生成的数据绑定代码会自动检查null值并避免空指针异常。例如,在表达式中@{user.name},如果 user为null,user.name则为其分配其默认值null。如果您引用user.ageage,其中age是类型int,则数据绑定使用默认值0。
集合
可以使用[]运算符访问普通集合,例如arrays、lists、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]}"
注意:您还可以使用object.key表示法引用map中的值。例如,@{map[key]}在上面的示例中可以替换为 @{map.key}。
字符串文字
您可以使用单引号括起属性值,这允许在表达式中使用双引号,如以下示例所示:
android:text='@{map["firstName"]}'
也可以使用双引号来包围属性值。这时,字符串文字应该用后引号括起来`:
android:text="@{map[`firstName`]}"
资源
可以使用以下语法访问表达式中的资源:
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 |
事件处理
数据绑定允许您编写从视图调度的表达式处理事件(例如,onClick()方法)。事件属性名称由监听器方法的名称确定,但有一些例外。例如,View.OnClickListener有一个方法onClick(),所以这个事件的属性是android:onClick。
对于click事件,有一些专门的事件处理程序需要除android:onClick避免冲突之外的属性。您可以使用以下属性来避免这些类型的冲突:
Class | Listener setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
您可以使用以下机制来处理事件:
- 方法引用:在表达式中,您可以引用符合监听器方法签名的方法。当表达式求值为方法引用时,Data绑定将方法引用和所有者对象包装在监听器中,并在目标view上设置该监听器。如果表达式求值为null,则数据绑定不会创建监听器并设置null监听器。
- 监听器绑定:这些是在事件发生时计算的lambda表达式。数据绑定总是创建一个监听器,它在view上设置。调度事件时,监听器会计算lambda表达式。
方法引用
事件可以直接绑定到处理程序方法,类似于android:onClick 可以分配给活动中的方法的方式。与View onClick属性相比的一个主要优点 是表达式在编译时处理,因此如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和监听器绑定之间的主要区别在于,实际的监听器实现是在绑定数据时创建的,而不是在触发事件时创建的。如果您希望在事件发生时评估表达式,则应使用监听器绑定。
要将事件分配给其处理程序,请使用普通绑定表达式,其值为要调用的方法名称。例如,请考虑以下示例数据对象:
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>
注意:表达式中方法的签名必须与监听器对象中方法的签名完全匹配。
监听器绑定
监听器绑定是在事件发生时运行的绑定表达式。它们类似于方法引用,但它们允许您运行任意数据绑定表达式。适用于Gradle版本2.0及更高版本的Android Gradle插件提供此功能。
在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有您的返回值必须与监听器的预期返回值匹配(除非它期望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>
在表达式中使用回调时,数据绑定会自动创建必要的监听器并注册事件。当View触发事件时,数据绑定会计算给定的表达式。与常规绑定表达式一样,在计算这些监听器表达式时,仍然可以获得数据绑定的null和线程安全性。
在上面的示例中,我们尚未定义view传递给的参数onClick(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表达式:
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,则你的表达式也必须返回相同类型的值。例如,如果要监听长按事件,则应返回表达式boolean。
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于null对象而无法计算表达式,则数据绑定将返回该类型的默认值。例如:引用类型为null对于参考, int为0,boolean为false等。
如果需要使用带断言的表达式(例如:三元运算),则可以使用void。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的监听器
监听器表达式非常强大,可以使您的代码非常容易阅读。另一方面,包含复杂表达式的监听器使您的布局难以阅读和维护。这些表达式应该像将UI中的可用数据传递给回调方法一样简单。您应该在从监听器表达式调用的回调方法中实现任何业务逻辑。
Imports、variables和includes
数据绑定库提供导入,变量和包含等功能。导入使布局文件中的类很容易引用。变量允许您描述可用于绑定表达式的属性。包括让您在整个应用中重复使用复杂的布局。
Imports
导入允许您轻松引用布局文件中的类,就像在托管代码中一样。import可以在data 元素内使用零个或多个元素。以下代码示例将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}"/>
Type aliases(类型别名)
当存在类名冲突时,可以将其中一个类重命名为别名。以下示例将包中的View类 重命名com.example.real.estate为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。
导入其他类
导入的类型可以用作变量和表达式中的类型引用。以下示例显示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中运行。您的应用程序仍在编译,您可以通过在变量定义中使用完全限定名称来解决IDE问题。
您还可以使用导入的类型来进行强转。以下示例将connection属性强制类型转换为User:
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在表达式中引用静态字段和方法时,也可以使用导入的类型。以下代码导入MyStringUtils类并引用其capitalize方法:
<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.*会自动导入。
variables(变量)
您可以variable在元素内使用多个元素data。每个 variable元素都描述了一个属性,该属性可以在布局中设置,以便在布局文件中的绑定表达式中使用。下面的示例声明的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>
在编译时检查变量类型,因此如果变量实现Observable或observable collection,则应该在类型中反射。如果变量的基类或接口未实现Observable接口,则不会观察变量。
当存在用于各种配置的不同布局文件(例如,横向或纵向)时,组合变量。这些布局文件之间不得存在冲突的变量定义。
生成的绑定类对于每个描述的变量都有一个setter和getter。调用setter之前,变量采用托管代码默认值 -- 引用类型为null,int为0,boolean为false等。
特殊变量context,可由绑定表达式生成。context的值是由根View的getContext()方法获取的Context对象。context变量由具有该名称的显式变量声明覆盖。
includes(包含)
通过使用app命名空间和属性中的变量名,变量可以从包含的布局传递到包含的布局绑定中。以下示例显示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>
数据绑定不支持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>
参考
[1] https://developer.android.com/topic/libraries/data-binding/
[2] https://developer.android.com/topic/libraries/data-binding/expressions
网友评论