谷歌推荐Data Binding实现MVVM模式(完整文档)

Data Binding 类库

这篇文档将教你如何运用 Data Binding 类库来编写声明试布局,并且尽量减少粘合代码对你的应用逻辑和布局上的绑定。
Data Binding 是一种灵活和广泛兼容的类库,它是一个支持库,因此你可以在任何 Android 2.1(API level 7+) 以上的设备 使用。
为了使用 Data Binding,Android Gradle 插件版本必须为 1.5.0-alpha1 或以上,查看 如何升级你的 Gradle 插件

构建环境

为了获取 Data Binding,去 Android SDK manager 下载 它的支持库。
在你的应用 module 的 build.gradle 添加 dataBinding 来让你的应用支持 Data Binding。
用以下代码片段来配置 Data Binding:

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

若你有一个应用 module 用了一个依赖了 Data Binding 的类库,也一样要在该 module 中配置开启 Data Binding。
另外,如果想使用 Data Binding,你们你的 Android Studio 版本必须等于或大于 1.3。


Data Binding 布局文件

编写你的第一个 Data Binding 表达式

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 标签下的 variable 是你在这个 Data Binding 布局文件中有可能使用到的对象。

<variable name="user" type="com.example.User"/>

布局中使用 @{} 语法来包裹 variable 中的对象属性,在下面例子中,TextViewtext 属性的值用 userfirstName 属性来替代。

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

数据对象

现在让我们假设你有一个普通的 Java 对象(POJO)User

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

这个对象属性(final 修饰)是不可变的,如果你的数据对象只提供只读权限并且之后不会再去修改的话,这种做法很普遍。我们也可以用 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} 来获取第一个类中的 fistName 字段值,活着获取第二个类中的 getFirstName() 方法返回的值。另外,如果 firstName() 方法存在的话也是可以获取到值的。

绑定数据

默认情况下,将根据布局文件的名称生成一个绑定类,将其转换为 Pascal 格式并将 Binding 作为其后缀。上面的布局文件是名称
main_activity.xml ,因此生成的绑定类是 MainActivityBinding。这个类将布局属性(例如用户变量)绑定到布局的视图中,并知道如何通过表达式来赋值。创建绑定类的最简单方式是在视图 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);
}

完成了!运行这个应用,你会在界面中看到测试的 User。另外,你可以通过一些方
式获取绑定类:

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

事件处理

数据绑定允许你编写表达式来处理从视图中分派的事件(例如 onClick)。除少数例外,事件属性名称由侦听器中的方法名称来确定。例如,View.OnLongClickListener 有一个 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){}
}

然后你可以绑定你的点击事件到你的类中,例如:

<?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 表达式作为根元素。 当表达式中有回调时,数据绑定会自动为事件创建必要的侦听器和注册表。 当视图触发事件时,数据绑定将评估给定的表达式。 就像在常规的绑定表达式一样,当这些监听器表达式被评估的时候,你仍然可以获取数据绑定的空值和保证线程安全。

请注意,在上面的例子中,我们没有定义传入 onClick(android.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,则表达式必须返回相同类型的值。 例如,如果你想监听长按事件,你的表达式应该返回布尔值。

public class Presenter {
    public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

如果由于空对象而无法评估表达式,Data Binding 将返回该类型的默认 Java 值。 例如,引用类型为 nullint0booleanfalse 等等。

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

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

避免复杂的监听器

监听器表达式非常强大,可以让你的代码变得非常容易阅读。 另一方面,包含复杂表达式的监听器也会使您的布局难以阅读和维护。这些表达式应该像从 UI 中传递可用数据到回调方法一样简单。你应该从侦听器表达式调用的回调方法内实现业务逻辑。
存在一些专门的单击事件处理程序,它需要除 android:onClick 之外的其他属性以避免冲突。 已创建了以下属性以避免这种冲突:

Class Listener Setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

布局文件细节

Imports

数据元素内可以使用零个或多个 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}"/>

如果类名有冲突的话,其中一个类则需起别名了。

<import type="android.view.View"/>
<import type="com.example.real.estate.View" alias="Vista"/>

现在,在布局文件中,Vista 被当作 com.example.real.estate.View 引入,View 被当作 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.* 会被自动导入。

Variables

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 collection,那么它应该被反映在类型中。 如果变量是没有实 Observable 接口的基类或接口,那么它将不会被观察!

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

生成的绑定类将为每个描述的变量设置一个 settergetter 方法。 变量将采用默认的 Java 值,直到调用 setter 为止 。对于引用类型为 null,对于 int0,对于 booleanfalse 等。

自定义绑定类的名字

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

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

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

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

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

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

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

Includes

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

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

表达式语言

共同特征

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

  • Mathematical + - / * %
  • String concatenation +
  • Logical && ||
  • Binary & | ^
  • Unary + - ! ~
  • Shift >> >>> <<
  • Comparison == > < >= <=
  • 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 中使用的一些表达式语法并不支持绑定操作。

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

空的合并运算符

空合并运算符 ?? 会选择左边的运算结果(如果它不是 null 的话)或右边的运算结果(如果它是 null 的话)。

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

这在功能上等同于:

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

属性引用

当一个表达式引用一个类的属性时,它对字段,setterObservableFields 使用相同的格式。

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

避免空指针异常

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

集合

通用的集合:数组,列表,SparseArray ,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)}"

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

Type Normal Reference Expression 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 对象(POJO)都可以用于数据绑定,但修改 POJO 不会导致 UI
更新。 数据绑定的真正威力在于通过给你的数据对象在数据改变时提供通知。 有三种不同的数据更改通知机制,Observable objects, observable fields, observable collections.

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

Observable Objects

实现 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 接口可以使用方便的 PropertyChangeRegistry 来实现,以有效地存储和通知监听器。

ObservableFields

创建 Observable 类需要做一点工作,所以想要节省时间或拥有很少属性的开发人员可以使用 ObservableField 及其同胞 ObservableBooleanObservableByteObservableCharObservableShortObservableIntObservableLongObservableFloatObservableDoublObservableParcelableObservableFields 是具有单个字段的独立的可观察对象。 原始版本在访问操作期间避免装箱和取消装箱。 要使用,请在数据类中创建一个公共 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

一些应用程序使用更多的动态结构来保存数据,观察集合允许对这些数据对象进行键值访问。当键是引用类型(如 String)时,ObservableArrayMap 非常有用。

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

在布局文件中,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

创建

应该在 inflate 之后立即创建绑定,以确保 View 层次结构不受干扰。 有几种方法可以绑定到布局。 最常见的是在绑定类中使用静态方法。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);

Views With IDs

将在布局中为每个视图生成一个公开的 final 字段。 该绑定在视图层次结构上执行单个传递,提取带有 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;

IDs 不像没有数据绑定那样必要,但是仍然有一些情况下代码需要访问视图。

变量

每个变量将被赋予访问器方法。

<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 与普通视图有点不同。 他们从不可见的时候开始,当他们要么变得可见时,要么被明确告知 inflate 时,他们通过 inflate 另一个布局来取代布局。

由于 ViewStub 本质上从视图层次中消失,所以绑定对象中的视图也必须消失以允许收集。 因为视图是 final 的,所以 ViewStubProxy 对象代替了ViewStub,当 ViewStub 存在时,开发人员可以访问 ViewStub,并且在 ViewStub被 inflate 时也可以访问被 inflate 的视图。

当 inflate 另一个布局时,必须为新的布局建立绑定。因此,ViewStubProxy 必须侦听 ViewStubViewStub.OnInflateListener 并在此时建立绑定。由于只有一个可以存在,ViewStubProxy 允许开发者在建立绑定之后设置一个 OnInflateListener 对象。

高级绑定

动态变量

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

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

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方法。 数据绑定框架可以自定义调用哪个方法来设置值。

自动的设置器

对于一个属性,数据绑定将试图找到设置属性的方法。属性的命名空间并不重要,只有属性名称本身才重要。例如,与 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}"/>

重命名设置器

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

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

开发人员不太可能需要重命名设置器, 安卓框架已经为这些属性实现了。

自定义设置器

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

安卓属性已经创建了 BindingAdapters。 例如,这里是 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}"/>

如果 imageUrl 和 error 都用于 ImageView 且 imageUrl 是字符串,并且 error 是 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);
        }
    }
}

上面的例子比正常情况稍微复杂一点,因为视图对侦听器使添加和删除,而不是对 View.OnAttachStateChangeListener 使用set方法。 android.databinding.adapters.ListenerUtil 类有助于跟踪以前的监听器,以便它们可以在绑定适配器中被移除。通过使用 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 注解接口 OnViewDetachedFromWindowOnViewAttachedToWindow,数据绑定代码生成器知道只应在 API 12 或以上的设备上调用 addOnAttachStateChangeListener(View.OnAttachStateChangeListener) 来运行运行侦听器。


转换器

对象转换

从绑定表达式返回一个对象时,将从自动,重命名和自定义的设置器中选择一个设置器。 该对象将被转换为所选设置器的参数类型。
这对于那些使用 ObservableMaps 来保存数据的开发者来说是很方便的。例如:

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

userMap 返回一个对象,该对象将被自动转换为在 setText(CharSequence) 中找到的参数类型。 当参数类型可能混淆时,开发者需要在表达式中输入。

自定义转换

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

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

在这里,背景需要一个 Drawable,但是颜色是一个整数。每当一个 Drawable 被判断该返回一个整数时,该整形应该被转换成一个 ColorDrawable。 这个转换是通过一个带有 BindingConversion 注解的静态方法完成的:

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

请注意,转换只发生在设置器级别,所以不允许混合类型,如下所示:

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

Android Studio 支持数据绑定

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 中所述。


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

熬夜写教程不容易,
如果你欣赏我的代码,
可以赞赏我几块钱买个新键盘。

发布了68 篇原创文章 · 获赞 128 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/gfg156196/article/details/98646223