(译)Data Binding 指南

anysort 9年前

来自: http://yanghui.name/blog/2016/02/17/data-binding-guide/

翻译自 http://developer.android.com/intl/zh-cn/tools/data-binding/guide.html

这个文档用于解释如何使用 Data Binding Library 编写声明式的布局,减少应用中逻辑以及布局所需要的“胶水代码”。

Data Binding Library 提供了灵活性与通用性 - 它是一个 support library,可以在 Android 2.1(API level 7+)以上的平台使用。

为了使用 data binding,gradle plugin的版本必须是 1.5.0-alpha1以上。

编译环境

为了使用 Data Binding,首先在 Android SDK manager 中下载最新的 Support repository。

然后在 build.gradle 中添加 dataBinding 段。

使用以下代码段配置 data binding:

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

如果你的 app module 依赖了一个使用 data binding 的库,那么你的 app module 的 build.gradle 也必须配置 data binding。

同时,确定使用了支持该特性的 Android Studio。在 Android Studio 1.3 以及之后的版本提供了 data binding 的支持,详见 Android Studio Support for Data Binding

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 标签中的 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}"/>

数据对象

假设你有一个 plain-old Java object(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;     }  }

这种类型的对象拥有不可改变的数据。在应用中,读一次且不变动数据的对象非常常见。也可以使用 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;     }  }

从 data binding 的角度看,这两个类是一样的。用于 TextView 的 android:text 属性的表达式 @{user.firstName} ,会读取 POJO 对象的 firstName 域以及 JavaBeans 对象的 getFirstName() 方法。此外,如果 firstName() 方法存在的话也同样可用。

绑定数据

在默认情况下,会基于布局文件生成一个 Binding 类,将它转换成帕斯卡命名并在名字后面接上”Binding”。上面的那个布局文件叫 main_activity.xml ,所以会生成一个 MainActivityBinding 类。这个类包含了布局文件中所有的绑定关系( user 变量),会根据绑定表达式给布局文件赋值。在 inflate 的时候创建 binding 的方法如下:

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

就这么简单!运行应用,你会发现测试用户已经显示在界面中了。你也可以通过以下这种方式:

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

如果你在 ListView 或者 RecyclerView 的 adapter 中使用 data binding,你可以这样写:

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

绑定事件

事件可以直接与 handler 函数绑定,类似于 android:onClick 可以指定 Activity 中的一个函数一样。事件属性的命名由 listener 的函数命名决定。举个例子, View.OnLongClickListener 中有一个 onLongClick() 函数,所以这个事件的对应属性就是 android:onLongClick 。

为了将事件分配给 handler,只需要使用一个 binding 表达式,值为要调用的函数名。举个例子,如果你的数据对象有两个函数:

public class MyHandlers {      public void onClickFriend(View view) { ... }      public void onClickEnemy(View view) { ... }  }

分配点击事件的 binding 表达式如下:

<?xml version="1.0" encoding="utf-8"?>  <layout xmlns:android="http://schemas.android.com/apk/res/android">     <data>         <variable name="handlers" type="com.example.Handlers"/>         <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="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>         <TextView android:layout_width="wrap_content"             android:layout_height="wrap_content"             android:text="@{user.lastName}"             android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>     </LinearLayout>  </layout>

也有一个特殊的点击事件 handler,他们有一些不同于 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 标签内可以有多个 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 在布局文件中同时使用。导入的类型也可以用于变量的类型引用和表达式中:

<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 还没有对导入提供自动补全的支持。你的应用还是可以被正常编译,要解决这个问题,你可以在变量定义中使用完整的包名。

<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.* 会被自动导入。

变量

data 标签中可以有任意数量的 variable 标签。每个 variable 标签描述了会在 binding 表达式中使用的属性。

<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* 接口的基类或借口,变量的变动则不会引起 UI 的变化!

当针对不同配置编写不同的布局文件时(比如横屏竖屏的布局),变量会被合并。所以这些不同配置的布局文件之间不能存在冲突。

自动生成的 binding 类会为每一个变量生产 getter/setter 函数。这些变量会使用 Java 的默认赋值,直到 setter 函数被调用。默认赋值有 null , 0 ( int ), false ( boolean )等。

binding 类也会生一个一个命名为 context 的特殊变量,这个变量被用于表达式中。 context 变量其实就是 rootView 的 getContext() ) 的返回值。 context 变量会被同名的显式变量覆盖。

自定义 Binding 类名

默认情况下,binding 类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加 “Binding” 结尾。这个类会被放置在 databinding 包中。举个例子,布局文件 contact_item.xml 会生成 ContactItemBinding 类。如果 module 包名为 com.example.my.app ,binding 类会被放在 com.example.my.app.databinding 中。

通过修改 data 标签中的 class 属性,可以修改 Binding 类的命名与位置。举个例子:

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

以上会在 databinding 包中生成名为 ContactItem 的binding 类。如果需要放置在不同的包下,可以在前面加 “.”:

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

这样的话, ContactItem 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中:

<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.xml 与 contact.xml 中都需要声明 user 变量。

Data binding 不支持直接包含 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 表达式有很多相似之处。下面是相同之处:

  • 数学计算 + - / * %
  • 字符串连接 +
  • 逻辑 && ||
  • 二进制 & | ^
  • 一元 + - ! ~
  • 位移 >> >>> <<
  • 比较 == > < >= <=
  • instanceof
  • 组 ()
  • 字面量 - 字符,字符串,数字, null
  • 类型转换
  • 函数调用
  • 域存取
  • 数组存取 []
  • 三元运算符 ?:

例子:

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

缺失的操作符

一些 Java 中的操作符在表达式语法中不能使用。

  • this
  • super
  • new
  • 显式泛型调用 <T>

Null合并运算符

Null合并运算符( ?? )会在非 null 的时候选择左边的操作,反之选择右边。

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

等同于

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

属性引用

首先是先前 编写你的第一个 data binding 表达式 中所提到的:JavaBean 引用。当表达式引用了一个类内的属性时,他会尝试直接调用域,getter,还有 ObservableFields。

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

避免NullPointerException

自动生成的 data binding 代码会自动检查和避免 null pointer exceptions。举个例子,在表达式 @{user.name} 中,如果 user 是 null, user.name 会赋予默认值 null 。如果你引用了 user.age ,因为 age 是 int 类型,所以默认赋值为 0。

容器类

通用的容器类:数组,lists,sparse lists,和 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]}"

字符串字面量

使用单引号把属性包起来,就可以很简单地在表达式中使用双引号:

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

也可以用双引号将属性包起来。这样的话,字符串字面量就可以用 &quot; 或者反引号(`) 来调用

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

一些资源需要显示类型调用。

Class Listener Setter Attribute
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

数据对象

任何 POJO 都能用在 data binding 中,但是更改 POJO 并不会同步更新 UI。data binding 的强大之处就在于它可以让你的数据拥有更新通知的能力。这里有三种不同的数据变动通知机制, Observable 对象observable 域 ,与 observable 容器类

当以上的 observable 对象绑定在 UI 上,数据发生变化时,UI 就会同步更新。

Observable 对象

当一个类声明了 Observable 接口时,data binding 会设置一个 listener 在绑定的对象上,以便监听对象域的变动。

Observable 接口有一个添加/移除 listener 的机制,但通知取决于开发者。为了简化开发,我们创建了一个基类 BaseObservable ,来实现 listener 注册机制。这个类也实现了域变动的通知,你只需要在 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 类会生成在 module 的 package 下。如果数据基类不可修改, Observable 接口的存储和 listener 通知可以用 PropertyChangeRegistry 来实现。

Observable域

创建 Observable 类还是需要花费一点时间的,如果开发者想要省时,或者数据类的域很少的话,可以使用 ObservableField 以及它的派生 ObservableBoolean ObservableByte ObservableChar ObservableShort ObservableInt ObservableLong ObservableFloat ObservableDouble ObservableParcelable 。 ObservableFields 是单一域的自包含 observable 对象。原始版本避免了在存取过程中做打包/解包操作。要使用它,在数据类中创建一个 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();  }

就这么简单!要存取数据,只需要使用 get set 方法:

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

Observable 容器类

一些应用会使用更加灵活的结构来保持数据。Observable 容器类允许使用 key 来获取这类数据。当 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 是整数类型时,可以使用 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<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"/>

生成Binding

生成的 binding 类将布局中的 View 与变量绑定在一起。就像先前提到过的,类名和包名可以 自定义 。生成的 binding 类会继承 ViewDataBinding

创建

binding 应该在 inflate 之后创建,确保 View 的层次结构不会在绑定前被干扰。绑定布局有好几种方式。最常见的是使用 binding 类中的静态方法。inflate 函数会 inflate View 并将 View 绑定到 binding 类上。此外有更加简单的函数,只需要一个 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

布局中每一个带有 ID 的 View,都会生成一个 public final 域。binding过程会做一个简单的赋值,在 binding 类中保存对应 ID 的 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>

将会在 binding 类内生成:

public final TextView firstName;  public final TextView lastName;

ID 在 data binding 中并不是必需的,但是在某些情况下还有有必要对 View 进行操作。

变量

每一个变量会有相应的存取函数:

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

并在 binding 类中生成对应的 getter setter:

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

ViewStub

ViewStub 相比普通 View 有一些不同。ViewStub 一开始是不可见的,当它们被设置为可见,或者调用 inflate 方法时,ViewStub 会被替换成另外一个布局。

因为 ViewStub 实际上不存在于 View 结构中,binding 类中的类也得移除掉,以便系统回收。因为 binding 类中的 View 都是 final 的,所以我们使用了一个叫 ViewStubProxy 的类来代替 ViewStub 。开发者可以使用它来操作 ViewStub,获取 ViewStub inflate 时得到的视图。

但 inflate 一个新的布局时,必须为新的布局创建一个 binding。因此, ViewStubProxy 必须监听 ViewStub 的 ViewStub.OnInflateListener ,并及时建立 binding。由于 ViewStub 只能有一个 OnInflateListener,你可以将你自己的 listener 设置在 ViewStubProxy 上,在 binding 建立之后, listener 就会被触发。

高级 binding

动态变量

有时候,有一些不可知的 binding 类。例如, RecyclerView.Adapter 可以用来处理不同布局,这样的话它就不知道应该使用哪一个 binding 类。而在 onBindViewHolder(VH, int) ) 的时候,binding 类必须被赋值。

在这种情况下,RecyclerView 的布局内置了一个 item 变量。 BindingHolder 有一个 getBinding 方法,返回一个 ViewDataBinding 基类。

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

直接 binding

当变量或者 observable 发生变动时,会在下一帧触发 binding。有时候 binding 需要马上执行,这时候可以使用 executePendingBindings() )。

后台线程

只要数据不是容器类,你可以直接在后台线程做数据变动。Data binding 会将变量/域转为局部量,避免同步问题。

属性 Setter

当绑定数据发生变动时,生成的 binding 类必须根据 binding 表达式调用 View 的 setter 函数。Data binding 框架内置了几种自定义赋值的方法。

自动 Setter

对一个 attribute 来说,data binding 会尝试寻找对应的 setAttribute 函数。属性的命名空间不会对这个过程产生影响,只有属性的命名才是决定因素。

举个例子,针对一个与 TextView 的 android:text 绑定的表达式,data binding会自动寻找 setText(String) 函数。如果表达式返回值为 int 类型, data binding则会寻找 setText(int) 函数。所以需要小心处理函数的返回值类型,必要的时候使用强制类型转换。需要注意的是,data binding 在对应名称的属性不存在的时候也能继续工作。你可以轻而易举地使用 data binding 为任何 setter “创建” 属性。举个例子,support 库中的 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 绑定在一起。举个例子, android:tint 属性可以这样与 setImageTintList(ColorStateList) ) 绑定,而不是 setTint :

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

Android 框架中的 setter 重命名已经在库中实现了,开发者只需要专注于自己的 setter。

自定义 Setter

一些属性需要自定义 setter 逻辑。例如,目前没有与 android:paddingLeft 对应的 setter,只有一个 setPadding(left, top, right, bottom) 函数。结合静态 binding adapter 函数与 BindingAdapter 注解可以让开发者自定义属性 setter。

Android 属性已经内置一些 BindingAdapter。例如,这是一个 paddingLeft 的自定义 setter:

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

Binding adapter 在其他自定义类型上也很好用。举个例子,一个 loader 可以在非主线程加载图片。

当存在冲突时,开发者创建的 binding adapter 会覆盖 data binding 的默认 adapter。

你也可以创建多个参数的 adapter:

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

imageUrlerror 存在时这个 adapter 会被调用。imageUrl 是一个 String,error 是一个 Drawable。

  • 在匹配时自定义命名空间会被忽略
  • 你可以为 android 命名空间编写 adapter

Binding adapter 方法可以获取旧的赋值。只需要将旧值放置在前,新值放置在后:

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

事件 handler 仅可用于只拥有一个抽象方法的接口或者抽象类。例如:

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

当 listener 内置多个函数时,必须分割成多个 listener。例如, 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);  }

因为改变一个 listener 会影响到另外一个,我们必须编写三个不同的 adapter,包括修改一个属性的,和修改两个属性的。

@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 可以用来辅助跟踪旧的 listener 并移除它。

对应 addOnAttachStateChangeListener(View.OnAttachStateChangeListener) ) 支持的 api 版本,通过向 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1) 注解,data binding 代码生成器会知道这些 listener 只会在 Honeycomb MR1 或更新的设备上使用。

转换器

对象转换

当 binding 表达式返回对象时,会选择一个 setter(自动 Setter,重命名 Setter,自定义 Setter),将返回对象强制转换成 setter 需要的类型。

下面是一个使用 ObservableMap 保存数据的例子:

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

在这里, userMap 会返回 Object 类型的值,而返回值会被自动转换成 setText(CharSequence) 所需要的类型。当对参数类型存在疑惑时,开发者需要手动做类型转换。

自定义转换

有时候会自动在特定类型直接做类型转换。例如,当设置背景的时候:

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

在这里,背景需要的是 Drawable ,但是 color 是一个整数。当需要 Drawable 却返回了一个整数时, int 会自动转换成 ColorDrawable 。这个转换是在一个 BindingConversation 注解的静态函数中实现:

<p>@BindingConversion public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); }</p>

需要注意的是,这个转换只能在 setter 阶段生效,所以 不允许 混合类型:

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

Android Studio 对 Data binding 的支持

Android Studio 支持 data binding 表达式的高亮,并会在编辑器中标出表达式中的语法错误。

在预览窗口显示的是 data binding 表达式的默认值。下面是一个设置默认值的例子, TextView 的 text 默认值为 PLACEHOLDER 。

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

如果你需要在设计阶段显示默认值,你可以使用 tools 属性代替默认值表达式,详见 设计阶段布局属性

</div>