Android官方文档之DataBinding库

本文与公众号三七文档库同步。

本文由三七原创翻译,转载前务必联系三七。

英文原文链接:https://developer.android.com/topic/libraries/data-binding/index.html

本文档介绍了如何使用DataBinding库来编写声明式布局,并尽量减少绑定应用程序逻辑和布局所需的中间代码。

DataBinding库提供了灵活性和广泛的兼容性 - 这是一个支持库,所以您可以在Android 2.1(API级别7+)之后的所有Android平台上使用它。

要使用数据绑定,Gradle 1.5.0-alpha1或更高版本的Android插件是必需的。请参阅如何为Gradle更新Android插件

构建环境

要开始使用数据绑定,请从Android SDK管理器的支持库中下载DataBinding库。

要配置应用程序以使用数据绑定,请将dataBinding元素添加到应用程序模块(module)的build.gradle文件中。

使用下面的代码片段来配置数据绑定:

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

如果您的应用程序模块依赖了使用数据绑定的库,则您的应用程序模块也必须在其build.gradle文件中配置数据绑定。

另外,请确保您使用的是Android Studio的兼容版本。 Android Studio 1.3及更高版本支持数据绑定,如Android Studio数据绑定支持中所述。

数据绑定编译器V2

3.1.0 Canary 6版本的Android Gradle插件附带一个可选的新编译器。要开始使用它,请更新您的gradle.properties文件以包含以下行:

  android.databinding.enableV2=true

在编译器v2中:

  • ViewBinding类是在java编译器之前由Android Gradle插件生成的。这可以避免由于不相关的原因使得java编译失败进而导致过多误报的错误。
  • 在V1中,编译应用程序时会重新生成库的绑定类(以共享生成的代码并访问最终的BRR文件)。在V2中,库保持其生成的绑定类以及映射器信息,这显著提高了多模块项目的数据绑定性能。

请注意,这个新的编译器是向后不兼容的,所以用v1编译的库不能被v2使用,反之亦然。

V2还会删除一些很少使用的功能来允许这些更改:

  • 在V1中,一个应用程序能够提供绑定适配器,可以覆盖依赖项中的适配器。在V2中,它只会在您自己的模块/应用程序及其依赖项中生效。
  • 以前,如果一个布局文件在两个或多个不同的资源配置中包含一个View具有相同id但不同类的数据,则数据绑定将查找最常见的父类。在V2中,当配置之间的类型不匹配时,它将始终默认为View
  • 在V2中,不同的模块不能在清单文件中使用相同的包名,因为数据绑定将使用该包名来生成绑定映射类。

数据绑定布局文件

编写您的第一套数据绑定表达式

数据绑定布局文件稍有不同,从布局的根标签开始,后跟数据元素和视图根元素。这个视图根元素跟非绑定式布局文件的根元素一样。示例文件如下所示:

<?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的文本被设置为userfirstName属性:

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

数据对象

现在让我们假设您有一个面向User的简单的Java对象(PO​​JO):

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

从数据绑定的角度来看,这两个类是等价的。用于TextViewandroid:text属性的@{user.firstName}表达式将访问前一个类中的firstName字段和后一个类中的getFirstName()方法。或者,如果firstName()方法存在,也将被解析。

绑定数据

默认情况下,将根据布局文件的名称生成一个Binding类,将其转换为Pascal格式并将Binding后缀添加到该文件中。上面的布局文件是main_activity.xml,生成类就是MainActivityBinding。这个类将布局属性(例如user变量)的所有绑定保存到布局的视图中,并知道如何为绑定表达式赋值。创建绑定的最简单方法是在填充布局时进行:

@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中的测试用户。或者,您可以通过以下方式获取视图:

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

如果您在ListViewRecyclerView适配器内使用数据绑定项目,则可能更愿意使用:

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

错误处理

数据绑定允许您编写表达式来处理从视图中分发的事件(例如onClick)。除少数例外,事件属性名称由监听器方法的名称来管理。例如,View.OnLongClickListeneronLongClick()有一个onLongClick()方法,所以这个事件的属性是android:onLongClick。处理事件有两种方法。

  • 方法引用: 在您的表达式中,您可以引用符合监听器方法签名的方法。当表达式评估为方法引用时,数据绑定将方法引用和所有者对象包装在监听器中,并将该监听器设置在目标视图上。如果表达式得出的值为null,则数据绑定不会创建监听器,而是设置空监听器。
  • 监听器绑定: 这些是在事件发生时被计算的lambda表达式。数据绑定总是创建一个监听器,它在视图上设置。事件分发时,监听器计算lambda表达式。

方法引用

事件可以直接绑定到处理方法,类似于android:onClick可以分配给Activity中的方法。与View#onClick属性相比,一个主要的优点是表达式在编译时被处理,所以如果方法不存在或者它的签名不正确,你会收到一个编译时错误。

方法引用和监听器绑定的主要区别在于实际的监听器实现是在绑定数据时创建的,而不是在事件触发时创建的。如果您喜欢在事件发生时计算表达式,则应该使用监听器绑定

要将事件分配给其处理程序,请使用常规的绑定表达式,其值是要调用的方法名称。例如,如果您的数据对象有两个方法:

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版及更高版本的Android Gradle插件。

在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有你的返回值必须与监听器的期望返回值相匹配(除非它预期为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表达式表示。在表达式中使用回调函数时,数据绑定会自动为事件创建必要的监听器和注册表。当视图触发事件时,数据绑定将计算给定的表达式。就像在常规的绑定表达式中一样,当这些监听器表达式被计算的时候,你仍然可以获得null和数据绑定的线程安全性。

请注意,在上面的例子中,我们没有定义传入onClick(android.view.View)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对象导致无法计算表达式,数据绑定将返回该类型的默认Java值。例如,null用于引用类型,0用于int类型, false用于boolean类型等。

如果您需要使用谓词(例如三元)表达式,则可以将void用作符号。

  android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的监听器

监听器表达式非常强大,可以让您的代码非常容易阅读。另一方面,包含复杂表达式的监听器会使您的布局难以阅读和维护。这些表达式应该像从UI中传递可用数据到回调方法一样简单。您应该在您从监听器表达式调用的回调方法内实现任意的业务逻辑。

存在一些专门的点击事件处理程序,它们需要一个属性, 以避免和android:onClick冲突。已经创建了以下属性以避免这种冲突:

监听器设置 属性
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

布局细节

导入

import元素中可以使用零个或多个data元素。这些就像在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.ViewView可能被用来在布局文件内引用android.view.View。导入的类型可以用作变量和表达式中的类型引用:

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

**注意:**Android Studio尚未处理导入,因此导入变量的自动填充可能无法在您的IDE中工作。您的应用程序仍然可以正常编译,您可以通过在变量定义中使用完全限定的名称来解决IDE问题。

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

变量

variable元素内可以使用任意数量的data元素。每个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集合,那么这个类型应该被描述出来。如果变量没有实现Observable* 接口的基类或接口,变量将不会被检查!

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

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

根据需要生成一个名为context的特殊变量用于绑定表达式。context值来自根视图的getContext()得到的Context。该context变量将被具有该名称的显式变量声明覆盖。

自定义绑定类名

默认情况下,根据布局文件的名称生成一个Binding类,以大写字母开头,删除下划线(_)并大写下一个单词的首字母,然后添加后缀“Binding”。这个类将被放置在模块包下的databinding包中。例如,布局文件contact_item.xml将生成ContactItemBinding。如果模块包是com.example.my.app,那么它将被放置在com.example.my.app.databinding

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

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

这将在模块包中的databinding包中生成绑定类ContactItem。如果该类应该在模块包内的其他包中生成,则可以用“.”作为前缀:

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

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

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

包含

通过在属性中使用应用程序命名空间和变量名称,变量可以从包含的布局传递到容器的布局的绑定中:

<?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作为合并元素的直接子元素。例如, 不支持以下布局:

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

表达式语言

共同特征

表达式语言看起来很像Java表达式。这些是一样的:

  • 数学的 + - / * %
  • 字符串连接 +
  • 逻辑运算符 && ||
  • 二进制 & | ^
  • 一元运算符 + - ! ~
  • 位运算 >> >>> <<
  • 比较运算符 == > < >= <=
  • instanceof
  • 分组 ()
  • 文字 - 字符, 字符串, 数字, null
  • 强转
  • 方法调用
  • 字段访问
  • 数组访问 []
  • 三元操作符 ?:

例子:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

缺少的操作

有些你在Java中能使用的表达式语法在这里会缺少一些操作符。

  • this
  • super
  • new
  • 明确的泛型调用

空合并运算符

null合并运算符(??)选择左边(如果不是null)或右边(如果为空)的操作。

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

这在功能上等同于:

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

属性引用

第一种已经在上面的编写您的第一个数据绑定表达式中讨论过了:简短形式的JavaBean引用。当一个表达式引用一个类的属性时,它对字段,getter方法和Observable字段使用相同的格式。

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

避免NullPointerException

生成的数据绑定代码自动检查空值并避免空指针异常。例如,在表达式中@{user.name},如果usernulluser.name将被赋予其默认值(null)。如果你是引用user.age,年龄是一个int,那么它将默认为0。

集合

常见的集合:数组,列表,稀疏列表(sparse lists),和映射集合(map),为了方便访问可以使用[]操作符。

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String&gt;"/>
    <variable name="sparse" type="SparseArray&lt;String&gt;"/>
    <variable name="map" type="Map&lt;String, String&gt;"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

字符串文字

在属性值周围使用单引号时,在表达式中使用双引号很容易:

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

也可以使用双引号来包围属性值。这样做时,字符串文字应该使用'或者反引号(`)。

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

数据对象

任何简单的Java对象(PO​​JO)都可以用于数据绑定,但修改POJO不会导致UI更新。当数据改变的时候,您的数据对象能够发出通知,这才是数据绑定的威力。有三种不同的数据更改通知机制: Observable对象observable字段, 和observable集合.

当这些observable数据对象之一被绑定到UI并且数据对象的属性改变时,UI将被自动更新。

Observable对象

实现Observable接口的类将允许附加单个监听器到绑定对象,以监听该对象上所有属性的更改。

Observable接口具有添加和删除监听器的机制,但通知由开发者决定。为了简化开发,创建了一个基类BaseObservable来实现监听器注册机制。数据类实现者仍然负责通知属性何时更改。这是通过给getter分配一个Bindable注解并通知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接口可以使用Observable interface may be implemented using the convenient PropertyChangeRegistry帮助类来是实现,以用于存储和高效地通知监听器。

ObservableField

一个小的工作是参与创建Observable类,所以想要节约时间的开发者可能有少数几个属性会用到ObservableField和它的同胞类ObservableBoolean, ObservableByteObservableChar, ObservableShortObservableInt, ObservableLongObservableFloat, ObservableDouble,和ObservableParcelableObservableFields是具有单个字段的独立observable对象。原始版本在访问操作期间避免装箱和取消装箱。要使用,请在数据类中创建一个公共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集合

一些应用程序使用更加动态化的结构来保存数据。Observable集合允许对这些数据对象进行键存取。当键是String等引用类型时ObservableArrayMap非常有用。

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

在布局中,可以通过String键访问map:

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap&lt;String, Object&gt;"/>
</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"/>

当键是一个整数时ObservableArrayList非常有用:

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

在布局中,列表可以通过索引来访问:

<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList&lt;Object&gt;"/>
</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"/>

生成的绑定

生成的绑定类将布局变量与布局中的视图链接起来。如前所述,绑定的名称和包可能是自定义的。生成的绑定类全部继承自ViewDataBinding

创建

应该在填充布局之后立即创建绑定类,以确保在将带有表达式的布局绑定到视图之前,View的层级不会被打乱。有几种方法可以绑定到布局。最常见的是使用Binding类中的静态方法。inflate方法填充了View的层级,并且一步就可以绑定到布局。有一个更简单的版本,只需要一个LayoutInflater和一个 ViewGroup

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

如果布局是使用不同的机制填充的,那么它可能会分开绑定:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

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

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

带有ID的视图

将在布局中为每个带ID的视图生成一个publicfinal字段。该绑定在View的层级上执行一次扫描,提取带有ID的视图。这个机制比调用多个视图的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的必要性比没有数据绑定时小,但是仍然有一些情况下需要通过代码访问视图。

变量

每个变量都会有访问的方法。

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

将会在绑定中生成setter和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和普通的视图有些不同。它们一开始是不可见的,当他们变得可见或者被明确告知需要填充时,它们会填充另一个布局来取代自己的布局。

由于ViewStub本质上在View的层级上是消失的,所以绑定对象中的视图也必须消失以允许收集。由于视图(View)是final类型的,所以用一个ViewStubProxy来代替了ViewStub,让开发者在ViewStub存在的时候可以访问它,并且在ViewStub填充的时候也可以访问被填充的视图层级。

在填充另一个布局的时候,必须为新的布局建立一个绑定。因此,ViewStubProxy必须监听ViewStubViewStub.OnInflateListener并且在那个时候建立绑定。由于只能存在一个ViewStub.OnInflateListener,所以ViewStubProxy允许开发者对它设置一个OnInflateListener,这样就会在建立绑定后调用这个监听。

高级绑定

动态变量

有时并不知道特定的绑定类是什么。例如,RecyclerView.Adapter正对任意布局的操作将不知道具体的绑定类。它仍然必须在onBindViewHolder(VH, int)的过程中分配绑定值。

在这个例子中,RecyclerView绑定的所有布局都有一个“item”变量。BindingHolder有一个getBinding方法返回ViewDataBindingViewDataBinding

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

即时绑定

当变量或observable变化时,绑定将被安排在下一帧之前改变。但有时候,绑定必须立即执行。要强制执行,请使用executePendingBindings()方法。

后台线程

只要不是集合,就可以在后台线程中更改数据模型。数据绑定将在计算时本地化每个变量/字段,以避免任何并发问题的出现。

属性Setter

每当绑定值发生变化时,生成的绑定类必须使用绑定表达式在视图上调用setter方法。数据绑定框架可以自主选择调用哪个方法来设置值。

自动化Setter

对于一个属性,数据绑定试图找到方法setAttribute。属性的命名空间并不重要,重要的是属性名称本身。

例如,与TextView属性相关联的表达式android:text将查找setText(String)。如果表达式返回一个int值,那么数据绑定将搜索setText(int)方法。请注意让表达式返回正确的类型,如果有必要的话就进行强制转换。请注意,即使给定名称的属性不存在,数据绑定也可以工作。然后,您可以使用数据绑定轻松地为任何setter**创建**属性。例如,支持库中的DrawerLayout没有任何属性,但是有很多setter。您可以使用自动化Setter来使用其中的一个。

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

重命名Setter

一些属性的setter跟属性名不匹配。对于这些方法,一个属性可以通过BindingMethods注解与setter相关联。每个重命名的方法的属性必须与一个类相关联,并包含BindingMethod注解。例如,android:tint属性确实与setImageTintList(ColorStateList)关联,而不是setTint

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

开发者不太可能需要重命名setter; android框架的属性已经实现了。

自定义Setter

一些属性需要自定义绑定逻辑。例如,该android:paddingLeft属性没有关联的setter 。取而代之的是setPadding(left, top, right, bottom)。带BindingAdapter注解的静态绑定适配器方法允许开发者自定义如何调用属性的setter。

Android属性已经创建了BindingAdapter。例如,这里是一个用于设置paddingLeft的例子:

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

绑定适配器对其他类型的自定义非常有用。例如,一个自定义的加载器可以被离线调用来加载一个图像。

当发生冲突时,开发者创建的绑定适配器将覆盖数据绑定的默认适配器。

您也可以让适配器接收多个参数。

@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);
}
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>

如果如果imageUrlerror都用于ImageView,并且imageUrl是字符串,error是drawable,则将调用此适配器。
This adapter will be called if both imageUrl and error are used for an ImageView and imageUrl is a string and error is a drawable.

  • 自定义命名空间在匹配时被忽略。
  • 您也可以为android命名空间编写适配器。

绑定适配器方法可以选择在其处理器中使用旧值。一个方法采用旧的还是新的值应该按照先使用旧值,再使用新值得顺序:

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

上面的例子比正常情况稍微复杂,因为视图在监听器中使用add和remove来代替View.OnAttachStateChangeListener中的set的方法。android.databinding.adapters.ListenerUtil类可以帮助跟踪以前的监听器,让他们可以在绑定Adaper时被移除。

通过给OnViewDetachedFromWindowOnViewAttachedToWindow接口添加@TargetApi(VERSION_CODES.HONEYCOMB_MR1)注解,数据绑定代码生成器就知道只在Honeycomb MR1和新设备生成监听器,由addOnAttachStateChangeListener(View.OnAttachStateChangeListener)支持相同的版本。

转换器

对象转换

从绑定表达式返回一个对象时,将从自动化,重命名和自定义setter中选择一个setter。该对象将被转换为所选setter的参数类型。

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

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

userMap返回一个对象,并且对象会自动转换为在setText(CharSequence)中发现的参数类型。当参数类型可能混淆时,开发者需要在表达式中进行强制转换。

自定义转换

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

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

在这里,背景需要一个Drawable,但颜色是一个整数。每当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"/>

Android Studio数据绑定支持

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

  • 语法高亮显示
  • 表达式语言语法错误的标记
  • XML代码完成
  • 参考,包括导航(比如导航到定义)和快速文档

注意:在没有错误的时候,数组和泛型类型(如Observable类)可能会显示错误。

Preview窗格显示数据绑定表达式的默认值(如果提供的话)。在下面的示例中摘录了布局XML文件中的一个元素,Preview窗格将显示PLACEHOLDER默认的文本值TextView

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

如果您需要在项目设计阶段显示默认值,还可以使用tools属性而不是默认表达式值,如设计时的布局属性中所述。

发布了30 篇原创文章 · 获赞 60 · 访问量 26万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览