谷歌数据绑定库 - 翻译

原文:https://developer.android.google.cn/topic/libraries/data-binding/index.html

本文档解释了如何使用数据绑定库(下面简称该库)来编写声明式布局,尽量减少绑定应用程序逻辑和布局所需要的耦合代码。

该库提供了灵活的兼容,最低可以兼容 Android 2.1(API 7)。

使用该库,需要 Gradle 1.5 以上,更新 Gradle 看这里

同时 Android Studio 版本也需要在 1.3 以上。


环境搭建(Build Environment)

第一步,下载该库

要使用数据绑定库,需要将 dataBinding 元素添加到 build.gradle 文件当中:

android {
    ....
    dataBinding {
        enabled = true
    }
}

如果你的应用模块也需要数据绑定,那么该模块的 build.gradle 也需要进行配置。


数据绑定编译器 V2(Data Binding Compiler V2)

Android Gradle Plugin 3.1.0 Canary 6附带一个可选的新编译器。 要想使用它,请编辑的 gradle.properties 文件添加下面内容:

android.databinding.enableV2=true

在 V2 中:

  • ViewBinding 类是在 Java 代码编译之前由 Android Gradle Plugin 生成的。这可以避免因为不相关的原因导致 java 代码编译失败。
  • 在 V1 中,编译应用程序时会重新生成库的绑定类(以共享生成的代码并访问最终的“BR”和“R”文件)。在V2中,库保存其生成的绑定类以及映射关系,这可以显著提高多模块项目的数据绑定性能。

注意,这个 V2 编译器向后不兼容,因此使用 V1 编译的库不能被 V2 使用,反之亦然。

V2还会删除一些很少使用的功能:

  • 在 V1 中,应用程序能够提供可以覆盖依赖项中的适配器的绑定适配器。在V2中,它只会对您自己的模块/应用程序及其依赖项中的代码生效。

  • 在之前的版本中如果两个或两个以上不同资源配置中的布局文件包含具有相同标识但不同类的 View,则数据绑定会查找最常见的父类。在 V2 中,当配置之间的类型不匹配时,它始终默认为 View。

  • 在 V2 中,不同的模块不能在清单中使用相同的包名称,因为数据绑定将使用该包名称来生成绑定映射器类。


数据绑定布局文件(Data Binding Layout Files)

编写你的第一个数据绑定表达式(Writing your first set of data binding expressions)

数据绑定布局文件和常规布局文件略有不同,它是由 layout 作为根布局,并包含data数据元素视图元素组成,这个视图元素就是你没有进行数据绑定时候的根布局,如下例:

<?xml version="1.0" encoding="utf-8"?>
<!-- layout 标签根布局-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- data 数据元素-->
   <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}"/>

数据对象(Data Object)

假设我们有一个普通的 User 对象(POJO)

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}

这种类型的对象有一个永远不会改变的数据。这种数据读取一次就永远不会更改。此外也可以使用 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;
   }
}

对于数据绑定来说,这两个类是等价的。TextView 当中的 android:text 属性使用 @{user.firstName} 来获取JOPO当中的 firstName 字段或者调用 JavaBean 当中的 getFirstName 方法,如果存在 firstName 方法,也可以解析为该方法。

绑定数据(Binding Data)

默认情况下,系统会根据布局文件的名称生成一个绑定类,将其转换成 Pascal 格式(也就是首字母大写) ,并添加后缀“Binding”。上面的布局文件是 main_activity.xml,所以生成的绑定类就是 MainActivityBinding,这个类包含了布局中所有 view 的属性,并且它还知道如何给绑定表达式分配值。创建绑定的最简单的方法是 inflate 时候进行绑定:

@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);
}

这就成了!运行程序,你就 UI 当中看到测试的 User 内容。你也可以通过这种方法获取 view:

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());

如果你在 ListView 或者 RecyclerView 适配器当中使用数据绑定,可以这样做:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

事件处理(Event Handling)

数据绑定运行用户编写表达式来处理 View 当中分派出来的事件(例如 onClick)。除少数例外,事件属性名称由监听器方法的名称来命名。例如,View.OnLongClickListener 中有 onLongClick 方法,所以这个事件的属性就是 android:onLongClick。处理事件有两种方法:

  • 方法引用:在表达式当中,可以使用复合监听器方法签名的方法。当一个表达式评估一个方法引用时,数据绑定将方法的引用和监听器中拥有的对象包裹起来并且将监听器设置给目标对象。如果表达式评估是 Null,数据绑定不会创建一个监听器而是会设置一个空的监听器。
  • 监听绑定:这些是当事件发生的时候的lambda表达式。数据绑定会在 View 上创建一个监听器。当事件被调度的时候,这个监听器就会求这个lambda表达式的值。
方法引用(Method References)

事件可以直接绑定到处理方法上,这和在 xml 当中使用 android:onClick 属性可以在 Activity 当中分配一个方法相类似。和 View#onClick 属性相比,主要优势是绑定表达式在编译时处理,所以如果该方法不存在或其签名不正确,则会收到编译错误。

方法引用和监听器绑定之间的主要区别在于实际的监听器是在数据绑定时创建的,而不是在触发时间时创建的。如果你希望你的事件在事件发生时去评估表达式,那么你应该是用监听器绑定。

要将事件分配给它的控制者,清使用一个标准的并且它的值是要调用的方法的名称的绑定表达式。例如,如果你的 data 对象有两个方法:

public class MyHandlers {
    public void onClickFriend(View 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>

注意,绑定表达式当中的方法签名必须与监听器对象当中的方法签名完全相同。

监听器绑定

监听器绑定是在事件发生时运行的表达式。它和方法引用类似,但它可以运行任意的数据绑定表达式。监听器绑定适用于 Gradle 2.0 以及更高版本当中使用。

在方法引用当中,事件监听器的参数必须和方法参数匹配。在监听器绑定当中,只需要你的方法返回值和监听器的期望返回值匹配(除非预期值为 void)。例如,你可以创建含有以下方法的 Presenter 类:

public class Presenter {
    public void onSaveClick(Task task){}
}

然后将 click 事件绑定到你的类中:

  <?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>

监听器只允许作为你的表达式的根元素的 lambda 表达式来表示。当表达式中使用回调时,数据绑定会自动为该事件创建所必须的监听器并且注册这个事件。当 View 触发这个事件的时,数据绑定就会计算给定的表达式。在常规绑定表达式当中,在评估这些监听器表达式时,你依旧可以获取数据绑定的空值并且保证线程安全性。

注意,在上例中,我们没有定义传入 onClick(android.view.View) 的参数。监听器绑定为监听器参数提供了两个选择:你可以忽略该方法的所有参数或者他们的名字。如果你更倾向于命名参数,你可以在表达式当中命名。例如,上面的表达式可以写成:

android:onClick="@{(view) -> presenter.onSaveClick(task)}"

如果你想在表达式中使用参数,它可以以如下方式工作:

public class Presenter {
    public void onSaveClick(View view, Task task){}
}

```xml
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

你还可以使用带有多个参数的 lambda 表达式:
```java
public class Presenter {
    public void onCompletedChanged(Task task, boolean completed){}
}

```xml

如果正在监听的事件返回值不是 void,那么表达式必须返回相同类型的值。例如,你需要监听长按事件,则表达式返回值应该是 boolean。
```java
public class Presenter {
    public boolean onLongClick(View view, Task task){}
}

```xml
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

如果表达式因为空对象而导致无法计算,数据绑定则会返回一个 Java 的默认类型的值。例如,引用类型返回 null,int 返回 0,boolean 返回 false 。

如果你需要在表达式当中使用带有断言的表达式(例如三元表达式),你可以使用 void:
```xml
 android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

避免复杂的监听器
监听器表达式非常强大,可以让你的代码非常容易阅读。但包含复杂表达式的监听器会使得你的布局难以阅读同时也难以维护。这些表达式应该 UI 当中传递可用数据到你的回调方法中一样简单。我们应该在侦听器表达式调用的回调方法中实现所有的业务逻辑。

有一些特殊的点击事件,他们需要一个 android:onClick 以外的属性来避免冲突。现已创建以下属性以避免此类冲突:

监听器设置android 属性
[SearchView](https://developer.android.google.cn/reference/android/widget/SearchView.html)[setOnSearchClickListener(View.OnClickListener)](https://developer.android.google.cn/reference/android/widget/SearchView.html#setOnSearchClickListener(android.view.View.OnClickListener))android:onSearchClick
[ZoomControls](https://developer.android.google.cn/reference/android/widget/ZoomControls.html)[setOnZoomInClickListener](https://developer.android.google.cn/reference/android/widget/ZoomControls.html#setOnZoomInClickListener(android.view.View.OnClickListener))android:onZoomIn
[ZoomControls](https://developer.android.google.cn/reference/android/widget/ZoomControls.html)[setOnZoomOutClickListener](https://developer.android.google.cn/reference/android/widget/ZoomControls.html#setOnZoomOutClickListener(android.view.View.OnClickListener))android:onZoomOut

布局详情(Layout Details)

导入(imports)

data 元素当中可以使用 0 个或多个 import 元素。这使得你可以在布局文件中轻松的引用类。就像java一样。

<data>
    <import type="android.view.View"/>
</data>

现在你可以在绑定表达式当中使用 View

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

当有类名冲突时,其中一个类需要使用 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。import 的类型可以用作变量和表达式的引用类型:

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List``User&gt;"/>
</data>

注意,AndroidStudio 还没有处理 import,所以在你的 IDE 当中自动 import 变量是不可行的,为了你的应用程序可以很好的编译,你可以通过在变量定义中使用完全限定名称来解决 IDE 存在的问题

<TextView
   android:text="@{((User)(user.connection)).lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

在表达式中使用静态字段和静态方法时,也可以使用 import 类型:

<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 中一样,java.lang.* 是自动导入的。

变量(Variables)

data 元素中可以使用任意数量的变量,每个变量描述了你可以使用在布局上的属性,该属性可以在布局中使用绑定表达式来设置。

<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 集合,那么它应该被反射。如果变量是一个没有实现 Observable* 接口的基类或者接口,这个变量将不会被观察到。

当各种配置(例如横向纵向)有不同的布局文件时,这些变量将会被合并。这些布局文件之间不能存在冲突的变量定义。

生成的绑定类将为每个描述的变量设置一个 settergetter。变量将采用默认的 Java 值,直到调用了 setter 为止,比如引用类型为 null,int 为 0,boolean 为 false。

根据需要生成一个名为 context 的特殊变量用于表达式。context 的值是根据根 View 的 getContext() 方法获取。该 Context 变量将被一个使用名字显示声明的变量覆盖。

自定义绑定类名(Custom Binding Class Names)

默认情况下,会根据布局文件的名称生成一个绑定类,以大写字母开头,去掉下划线,并且后面跟着的单词首字母大写,然后添加 Binding 后缀)。生成的绑定类放在模块包下的 databinding 包当中。例如 contact_item.xml 将会生成 ContactItemBinding 绑定类,如果模块包是 com.example.myapp,那么该类的位置是 com.example.myapp.databinding

通过调整 data 元素的类属性,绑定类可以重命名或者放置在不同的包当中。例如:

<data class="ContactItem">
    ...
</data>

这会在模块包中的 binding 包中生成绑定类 ContactItem。如果该类应该在模块包中的其他包中生成,则它需要以 . 作为前缀:

<data class=".ContactItem">
    ...
</data>

在这种情况下,直接在模块包中生成 ContactItem。如果提供完整的包名,则可以生成在任意包中:

<data class="com.example.ContactItem">
    ...
</data>

包含(includes)

通过使用应用命名空间和属性中的变量名字,可以从容器布局中传递变量到一个被include的布局中:

<?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>

在这里,name.xmlcontact.xml 布局文件都必须拥有一个 user 变量。

数据绑定不支持 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>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

表达式语法(Expression Language)

共性(Common Features)

表达式语法和 Java 语法很像,相同点:

  • 数学运算:+ - / * %
  • 字符串连接:+
  • 逻辑运算:&& ||
  • 位运算:& | ^
  • 一元运算:+ - ! ~
  • 位移:>> >>> <<
  • 比较:== > < >= <=
  • 比对对象类型: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}'
(和 Java 相比)缺失的操作符(Missing Operations)

有一些 Java 中使用的操作符在绑定表达式中是没有的:

  • this
  • super
  • new
  • Explicit generic invocation(显示泛型调用)
空聚合操作符(Null Coalescing Operator)

null 聚合操作符 ??,如果它为空,则选择右边;如果不为空,则选择左边:

android:text="@{user.displayName ?? user.lastName}"

其功能和下面相同:

android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用(Property Reference)

在上面关于 JavaBean 的使用部分已经讨论过了如果编写你的第一个数据绑定表达式。当表达式引用了一个类的属性时,他将对字段、getter 和 ObservableFields 使用相同的格式。

android:text="@{user.lastName}"
避免空指针(Avoiding NullPointerException)

生成数据绑定代码会自动检查空值并避免空指针异常。例如在表达式 @{user.name} 当中,如果 usernull,那么 user.name 将被分配为默认值:null。如果你引用了 user.age,那么 age 是一个 int,那么它将默认为 0

集合(Collections)

常见的集合: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]}"
字符串(String Literals)

当属性值的前后两边使用单引号 ' 时候,你就可以在表达式中使用双引号 ":

android:text='@{map["firstName"]}'

也可以使用双引号 " 来包裹属性值,这样时,字符串文字应该使用单引号 ' 或者反引号 `

android:text="@{map[`firstName`}"
android:text="@{map['firstName']}"
资源(Resources)

使用正常语法可以将资源作为表达式的一部分进行访问:

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)}"

有些资源需要明确的类型评估:

TypeNormal ReferenceExpression Reference
`String[]``@array``@stringArray`
`int[]``@array``@intArray`
`TypedArray``@array``@typedArray`
`Animator``@animator``@animator`
`StateListAnimator``@animator``@stateListAnimator`
`color int``@color``@color`
`ColorStateList``@color``@colorStateList`

数据对象(Data Objects)

任何 Java 对象(JOPO)都可以用于数据绑定,但修改 JOPO 不会更新 UI。数据绑定的强大就在于它能够让你的数据拥有更新通知的能力。有三种数据变动通知机制: Observable objects,observable fields, 和 observable collections。

当这些 Observable 数据对象之一绑定到 UI 之后,如果数据对象的属性值改变了,UI 也会自动更新。

可观察对象(Observable Objects)

一个实现了 Observable 接口的类允许 binding 将单个监听器添加到绑定对象上,以监听对象上所有属性的变化。

Observable 接口具有添加和删除监听器的机制,但通知是由开发者去管理。为了使开发更加简单,创建一个基类 BaseObservable 以实现监听器注册机制。数据类的实现类依旧负责通知属性何时更改。这是通过添加一个 Bindable 注解给数据类的 getter 和 setter,当属性改变时,数据类仍然可以响应通知。

private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

在编译的时候,Bindable 注解在 BR 类文件中生成一个条目。在模块包下生成一个 BR 文件。如果数据类是不能改变,Observable 接口会通过 PropertyChangeRegister 来高效的存储和通知监听器。

可监听字段(ObservableFields)

创建 Observable 类需要做一点工作,所以希望节省时间或者属性较少的情况下可以使用 ObservableField 或者它的同类 ObservableBooleanObservableByteObservableCharObservableShortObservableIntObservableLongObservableFloatObservableDoubleObservableParcelable。ObservableField是一个具有单个字段的完整的可观察对象,最初的版本是避免在操作时做装箱和拆箱操作。在数据类中以 public final 字段形式创建并使用:

private static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

妥了!要访问这些值,请使用 set 和 get 方法:

user.firstName.set("Google");
int age = user.age.get();

可观察的集合(Observable Collections)

一些程序更多的使用动态结构去控制数据。Observable Collections 允许键值对的形式去获取数据。当 key 是引用类型时(例如 String),ObservableArrayMap 是非常有用的。

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在布局中,使用 String key 去访问 map:

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

当 key 为 Integer 时,ObservableArrayList是非常有用的:

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);

在布局中,list 可以通过索引访问到:

<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
   android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

生成的绑定(Generated Binding)

生成的绑定类将布局变量和布局中的 view 连接起来。就和前面说的一样,绑定的名称和包都可以自定义。生成的绑定类都继承了 ViewDataBinding

创建(Creating)

绑定需要在 inflate 之后立即创建,用以确保在使用布局中的表达式绑定到 View 之前 View 的层次结构不被干扰。有集中方法可以绑定到布局。最常见的是在 Binding 类上使用静态方法。inflate 方法 inflate view 的层级并绑定它,所有操作,一步完成。有一个简单的版本,只需要一个 LayoutInflater 和一个 ViewGroup

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);

如果布局使用不同的机制去 inflate,则可能需要单独绑定:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有时绑定不能预先得知,在这种情况下,可以使用 DataBindingUtil 类创建绑定:

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

具有 ID 的 View(Views With IDs)

在 layout 中为每一个具有 ID 的 View 生成 public final 字段。绑定在 View 层级中进行单一传递,提取具有 ID 的 View,这个机制比调用每个 View 的 findViewById 更加快速,例如:

<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}"
   android:id="@+id/firstName"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
  android:id="@+id/lastName"/>
   </LinearLayout>
</layout>

会生成一个绑定类:

public final TextView firstName;
public final TextView lastName;

ID 在数据绑定中几乎没有必要,但仍有一些情况下代码需要通过 ID 去访问 view。

变量(Variables)

每个变量都会被赋予一个访问方法:

<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>

会在绑定中生成 seeter 和 getter 方法:

public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);

ViewStubs

ViewStub 和普通 View 不同。他们一开始是不可见的,当 ViewStub 被设置为可见或者明确调用 inflate 时,ViewStubs 将会通过 inflate 一个其他 layout 来替换本身。

因为 ViewStub 基本上从 View 的层次结构中消失,View 当中的绑定对象也必须消失以便回收。因为那个 View 是 final 的,当 ViewStub 已经被 inflated 且在 ViewStubProxy 对象取代 ViewStub 的位置时,如果获得了 View 的层级的 ViewStub 是存的,开发者便能获得该 ViewStub。

在 inflating 另一个布局时,对于一个新的布局,一个绑定必须是明确的。因此,ViewStubProxy 必须监听 ViewStubs 的 ViewStub.OnInflateListener 并且明确这绑定的时间。ViewStubProxy 只允许开发者去设置一个 OnInflateListener,在明确地绑定时,该监听者将被调用。


高级绑定

动态变量

有时候,特定的绑定类将会被隐藏。例如
一个 RecyclerView.Adapter 对任意布局操作不会知道具体的binding类。它仍然必须在onBindViewHolder(VH, int)期间分配binding值。

在这个例子中,RecyclerView绑定的所有的布局度有一个"item"的变量。BindinngHolder 包含一个 getBinding 方法,该方法返回基本的 ViewDataBinding

public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

即时绑定(Immediate Binding)

当一个变量或者observable发生变化的时候,binding将被安排在下一帧之前进行更改。但是有时必须立即执行binding。可以使用 executePendingBindings() 方法强制执行。

后台线程(Background Thread)

只要你数据不是一个集合,你就可以在后台线程当中去修改它。数据绑定会将每个变量/字段本地化来避免任何并发时产生的问题。

属性设置(Attribute Setters)

当绑定的值发生变化时,生成的绑定类必须使用绑定表达式在 view 上调用 setter 方法。数据绑定框架有自定义哪个方法来调用以设置值的方法。

自动设置(Automatic Setters)

对于一个 Attribute,数据绑定会尝试去查找 setAttribute 方法。和属性的命名空间没有关系,只和属性的名字有关。

例如和 TextView 的 android:text 属性相关联的表达式将查找 setText(String) 方法。如果表达式返回一个 int,则数据绑定将搜索 setText(int) 方法。注意让表达式返回正确的类型,如果需要的话,可以进行强制类型转换。请注意,即使给定名称不存在任何属性,数据绑定也会其作用。然后,你可以通过使用数据绑定轻松为任何的 setter 方法创建属性。例如,support 包的 DrawerLayout 没有任何属性,但含有大量的 setter 方法。你可以使用自动设置来创建他们(属性)。

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

重命名设置(Renamed Setters)

一些属性具有和他属性名不匹配的 setter 方法,对于这些方法,可以通过 BindingMethods 注释将属性和 setter 方法相关联。对于每个重命名的方法,该方法必须属于一个类并且包含 BindingMethod 注解。例如,android:tint 属性实际上是和 setImageTintList(ColorStateList) 方法相关联,而不是 setTint 方法。

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

开发人员不太可能需要重命名 setter 方法, android 框架的属性已经实现了。

自定义设置(Custom Setters)

一定属性需要自定义绑定逻辑,例如 android:paddingLeft 属性没有关联 setter 方法,相反 setPadding(left, top, right, bottom) 方法存在。使用 BindingAdapter 注解的静态 binding 设配器方法允许开发人员以定制如何调用属性的 setter 方法。

Android 属性已经创建了 BindingAdapters,下面以 paddingLeft 为例:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

BindingAdapter可以用于其他类型的定制。例如,自定义的 loader 可以用于异步加载图像。

发生冲突时,我们创建的BindingAdapter将覆盖默认的data binding adapters。

也可以让适配器接受多个参数:

@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}

```xml

如果 imageUrl 和 error 都用作 ImageView ,并且 imageUrl 是一个 string 类型并且 error 是一个 drawable 对象时,该适配器会被调用。

 - 自定义命名空间在匹配过程当中会被忽略。
 - 也可以为 android 命名空间编写适配器。

Binding adapter 方法可以选择在它的 handler 中获取旧值。获得旧的值和新的值的时候,首先获得的是该属性的所有旧的值,然后才是新的值。

```java
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
   if (oldPadding != newPadding) {
       view.setPadding(newPadding,
                       view.getPaddingTop(),
                       view.getPaddingRight(),
                       view.getPaddingBottom());
   }
}

事件处理程序只能用于一个抽象方法的接口或抽象类,例如:

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue);
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue);
        }
    }
}

当一个监听器有多个方法时,必须被拆分为多个监听器。例如 View.OnAttachStateChangeListener 有两个方法:onViewAttachedToWindow()onViewDetachedFromWindow()。我们必须为该属性创建不同的接口去区分属性并处理他们。

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
    void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
    void onViewAttachedToWindow(View v);
}

因为改变一个监听器也会影响另一个监听器,所以我们必须有三个不同的绑定适配器,如果它们都被设置,其中两个分别用于各个属性,另外一个同时用于两个属性。

@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
    setListener(view, null, attached);
}

@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
    setListener(view, detached, null);
}

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
        final OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        final OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }
        final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
                newListener, R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

上面的例子稍微复杂一些,因为 View 使用 add 和 remove 来替代 View.OnAttachStateChangeListener 的 set 方法。android.databinding.adapters.ListenerUtil 类可以帮助跟踪以前的监听器,以便他们可以在在绑定适配器中删除它们。

通过使用 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 注解 OnViewDetachedFromWindowOnViewAttachedToWindow 接口,数据绑定代码生成器会知道这些监听器只能在 Honeycomb MR1 或者新设备(和 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)支持的版本相同)上使用。

转换器(Converters)

对象转换器(Object Cpmversions)

当绑定表达式返回对象时,setter 方法会在自动 setter、重命名 setter 以及自定义 setter 中选择。该对象将会转换为所选 setter 方法的参数类型。

这对于那些使用 ObservableMaps 来保存数据的人来说很方便。 例如:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

userMap 返回一个Object,并且该Object将自动转换为在 setText(CharSequence)中找到的参数类型。当参数类型可能产生混淆时,开发者需要在表达式中手动做类型转换。

自定义转换器(Custom Conversions)

有时转换应该在特定的类型之间自动进行,例如,在设置背景时:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

在上面的例子中,背景需要一个 Drawable,但 color 是一个 integer。每当需要一个 Drawable 但返回一个整数时,int 应该转换为 ColorDrawable。 这种转换是使用带有 BindingConversion 注释的静态方法完成的:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

注意,转换只在 setter 方法里触发,所以转换不允许像下面这样的混合类型。

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

AndroidStudio 对数据绑定的支持(Android Studio Support for Data Binding)

Android Studio 支持许多用于数据绑定代码的编辑功能。例如,它支持数据绑定表达式的以下功能:

  • 语法高亮
  • 标记错误的表达式语法
  • XML 代码完成
  • 参考资料,包括导航(譬如导航到声明),快速文档

注意,数组和泛型类型,例如 Observable 类可能在没有错误时显示错误。

“预览”窗口显示数据绑定表达式的默认值(如果有的话)。在以下示例中,从布局 XML 文件中截取元素,“预览”窗口在 TextView 中显示 PLACEHOLDER 默认文本值。

<TextView android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{user.firstName, default=PLACEHOLDER}"/>

如果需要在项目的设计阶段显示默认值,则还可以使用工具属性而不是默认表达式值,如Design Time Layout Attributes中所述。

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1