All Articles

Android Databinding 数据绑定

2017.8.15 初次添加 017.12.20 更新BindingMethod注解

如果你是第一次使用强烈推荐你去读这几篇文章  Android databinding(初识)  Android databinding详解(一)—layout解析 Android databinding详解(二)—activity解析 Android databinding(详解三)—自定义属性使用 Android databinding(四)—layout中的特殊使用  Xml文件必须转义的字符

官方数据绑定库的翻译 和原文有所不同是按照我的理解翻译的 https://developer.android.com/topic/libraries/data-binding/index.htm

数据绑定库

这篇文档介绍了如何使用Data Binging库写声明式的布局,使用最少的胶水代码将业务逻辑和界面绑定在一起。

这个Data Binding库具有灵活性和高兼容性的特点—它是一个support库,你可以在不低于安卓2.1版本(API level 7+)上使用。

构建环境


使用安卓 SDK manager 从 Support 库下载 Data Binging ,并在 app 模块下的build.gradle文件中添加dataBinding的配置文件。 使用下面的代码片段配置data binding:

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

* 即使你的app模块依赖的库使用了data binding,你也需要在app模块的build.gradle文件中添加data binding的配置。 同样的你也需要使用不低于 Android Studio1.3 的 Android Studio 版本,提供了对data binding的支持。

Data Binding 布局文件


第一个data binding表达式

data binding的布局文件和普通的布局文件有一点不同,它的根布局是在根布局下面跟一个元素和一个view的布局元素。下面是一个简单的示例。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

在 data 标签 variable 中声明的 user 可能作为一个属性使用到布局文件中

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

布局中的表达式需要写在@{}语法中,下面的示例中演示了如何将 user 的 firstName 作为 TextView 的文本:

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

数据对象

假设你的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;
   }
}

这是一个不可变对象。 这个对象一旦生成就不能改变,当然它也可以是这样:

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 的 text 上使用的表达式@{user.firstName}会访问前一个对象的 firstName 属性,访问后一个对象的 getFirstName() 方法。实际上如果firstName()存在的话它也会被解析成 firstName() 方法。

绑定数据

默认情况会生成一个 Bingding 类,这个类会根据布局文件生成一个PascalCase命名方式(首字母大写)以“Binding”为后缀的名字。上面布局文件的名字是main_activity.xml所以生成的类为MainActivityBinding。这个类包含了所有的视图数据(例如user变量)到视图的 binding ,并且知道怎么从 binding 表达式取值设置到 View。 创建 binding 的最简单的方式是在 inflating 的时候:

@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界面上看到Test 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);

事件处理

Data binding 允许你使用表达是处理 View 的事件(例如 OnClick)。 事件属性的名字和监听事件方法相关但是有一点区别。例如,View.OnLongClickListener 接口有一个方法 onLongClick() ,所以这个事件的属性名为 android:onLongClick 。这里有两种处理事件的方式:

  • 方法引用: 在布局文件的表达式中添加处理监听事件的方法的签名。当表达式检测为一个方法引用,Data binding 将会把这个方法引用和它的所属对象包装进一个 listener,并将这个 listener 设置到目标View。如果表达式检测为null, Data Binding 将什么也不做。
  • 监听绑定:使用 lambda 表达式处理事件,Data Binding会创建一个 listener 设置到目标 View。当事件触发的时候会调用这个 lambda 表达式。

方法引用

事件会直接调用处理方法,就像android:onclick会调用对应Activity的方法一样。和View#onClick相比表达式会在编译器被处理,所以方法不存在或者方法签名书写错误可以在编译器发现。

和 事件绑定 相比 方法引用 的主要区别是 listener 的实现在数据绑定的时候就生成,而不是事件触发的时候。如果你更倾向于在事件触发的时候调用表达式你应该使用事件绑定。

需要生成一个处理事件的对象,像下面这样

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+上才能使用。

使用方法引用,方法的参数必须和事件listener的参数相同。使用事件绑定,你的方法返回值必须和事件需要的返回值相同(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>

listener 被替换成了 lambda 表达式,但是 lambda 只能在表达式的根元素使用。当使用表达式代替回调时,Data Binding 会为这个事件创建所需的 listener 和 register 。当 View 触发事件的时候 Data Binding 会检查所写的表达式。

As in regular binding expressions, you still get the null and thread safety of Data Binding while these listener expressions are being evaluated. (这句话没理解,好像是说检查表达式的时候是空安全而且线程安全的)

* 注意下面的例子中,没有将onclick(android.view.View)传过来的view添加到onsaveClick()参数中。事件绑定提供了两种方式来处理 listener 的参数:全部忽略或全部命名。如果你更倾向于命名这些参数,你可以将添加到表达式中,就像下面写的这样:

 <... 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,表达式的返回值必须和事件的返回值一致。就像下面这样你监听的是onLongClick()事件,它的返回值是boolean

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

如果由于表达式的返回值为null对象,Data Binding 将返回默认的 java 值(引用将返回null,int 返回 0, boolean返回false 等)。 如果你使用了谓语表达式(例如三目运算),你可以使用void作为标识符。

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

避免复杂的监听

监听表达式是非常强大的它可以使你的代码变得易读。另外,listener 中含有复杂的表达式会使布局不易阅读和维护。表达式应该只做数据的传递,在方法的内部处理业务逻辑。 一些特殊的点击事件存在和`android:onclick冲突的地方,需要使用下面的属性避免冲突:

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

布局细节


<import>标签

<data>标签下可能存在零个或多个impoert标签,允许在你的布局文件中使用像java一样的索引。

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

布局文件中就可以使用这些导入的类型:

<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。导入的类型也可以在<Variables>中使用,注意<>需要替换成&lt; &gt;

<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 可能在<variables>中无法使用自动补全功能。但是不影响应用编译,你也可以在<variables>中使用全限定符(fully qualified names)来解决这个问题。

<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.lang.*是自动导入的。

<Variable>标签

<data>标签下可以有任意数量的<variable>标签。每个<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 它就会在变量改变的时候被通知到。

因为配置(例如横竖屏)问题而创建的不同布局文件中的声明的<variable>变量会被组合在一起。所以这些布局文件中声明的变量不能存在冲突。

生成的 binding 类会为每个声明的变量生成 getter 和 setter 方法,这些变量在调用 setter 之前保存的是 java 默认值。

如果表达式的使用到context,就会生成一个特殊(隐式)的context变量, 这个 context变量从 root View 的getContext()获取的。如果context被显式声明那么这个隐式context会被覆盖。

<TextView
   android:text="@{context.getPackageName()}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

自定义 Binding 的类名

默认情况下 Binding 的类名是根据布局文件名生成的(首字母大写,删除下划线,以Binding结尾),这个类会被放在module的包名下的databinding文件夹下面。举个例子,如果你的布局文件名是contact_item.xml那么生成的 Binding 类名就为ContactItemBinding。如果module的包名是com.example.my.app那么这个 Binding 类就会被放在com.example.my.app.databinding文件夹下。

Binding 类名也可以被重新定义,只需要修改<data> 标签的 class 属性,就像下面这样:

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

这样就会在 module 包文件下面的 databinding 文件夹下面生成一个 binding 类,这个类名为ContactItem。如果你想将 binding 类生成在 module 包文件的相对路径下(而不是databinding文件),你需要添加“.”前缀像下面这样。

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

这样ContactItem就会被放在module的包名文件夹下。当然你也可以使用完整的包名来控制binding的位置:

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

View视图下面的include标签

<data>中声明的变量可以通过View视图中的include标签传递给它的子视图,需要在布局文件中声明应用命名空间(xmlns:bind="http://schemas.android.com/apk/res-auto"),在传值时使用变量名bind:user

<?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 不支持 <mege> 标签的直接子标签中使用 <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
  • 括号 ()
  • Literals - character, String, numeric, null
  • 强制转换
  • 方法调用
  • 属性访问
  • 数组访问 []
  • 三目运算 ?:

例如:

<...
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'/>
不支持的语法

java 语法中的一些操作符是不支持的

  • this
  • supper
  • new
  • 显式泛型
空聚合运算符

空聚合运算符(??)在左面表达式不为空的情况下会取左边的表达式的值否则会选择右边的表达式的值

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

它等同于下面的表达式

<... android:text="@{user.displayName != null ? user.displayName : user.lastName}"/>
属性引用

上面的内容已经讨论过了:它是 JavaBean 的一种简短引用方式。当在表达式中使用了一个类的属性引用,就会从相同格式的属性, getter 方法和 ObservableFilds 中取值。

<... android:text="@{user.lastName}"/>
避免空异常

自动生成的 data binding 代码会检查空和空指针异常。例如:在@{user.name}中如果user为空,user.name会被设置为它的默认值(null)。如果你使用user.age并且age的类型是int那它会被设置成默认的0。

集合

通用的集合类型: arrays, lists, sparse lists, 和 maps, 可以使用[ ]操作符来调用其中的值。

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List<String>"/>
    <variable name="sparse" type="SparseArray<String>"/>
    <variable name="map" type="Map<String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
字符串的字面值

如果你想在表达式中使用字符串的字面值可以在外层使用单引号,字符串使用双引号。

<... android:text='@{map["firstName"]}' />

也可以在外层使用双引号内曾使用单引号或者back quote (`)[tab键上面的那个键]

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

数据对象


任何的(POJO)Java 对象都可以在 data binding 中使用,但是修改相应的 POJO 并不会更新对应的 UI 视图。而 data bingding 可以给你的数据对象在数据改变时更新 UI 的能力。有三种数据改变的更新机制:Observable objects, Observable fields, 和 Observable collections。

当使用上面方法中的任意一种方式集成到 data binding 这个数据就有了自动更新 UI 的能力。

Observable Objects

实现 Observable 接口的类,binding 将会为这个类创建一个 listener 来监听这个类中所有属性的改变。

Observable接口提供了一个机制来添加 删除 listeners, 但是是否通知取决于开发者。为了简化开发,可以通过继承 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 class 文件,这个文件会被放在 module 包下。 如果你的数据类继承了其他的类,可以将Observable的接口交给PropertyChangeRegistry处理,它集成起来很方便,而且可以高效的存储和通知listeners。

ObservableFields

在创建Observable类的时候还是需要开发者自己写一些其他的代码的,所以如果你想节约时间或者只有很少的属性,你可以使用 ObservableField 或者 ObservableBooleanObservableByteObservableCharObservableShort, ObservableIntObservableLongObservableFloatObservableDouble, 和 ObservableParcelable 。 ObservableFields是独用的observable对象,它只有一个属性。其他的对象(ObservableBoolean,ObservableByte…)可以避免访问基础类型造成的装箱拆箱。可以通过创建一个 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 Collections

一些应用使用更动态的结构是有数据。 Observable collections允许用键来访问数据,当键是引用类型的时候使用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<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"/>

当键是整型的时候可以使用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和variables连接在了一起。就像上文说的那样类的名字和包名可以自定义。所有生成的类都继承自 ViewDataBinding

创建

这个 binding 应该在 inflation 之后尽快的创建,以确保视图层次结构在与布局中的表达式绑定到视图之前不会受到干扰。和布局绑定有几种方式,通用的方式是使用 Binding 类的静态方法。inflate 方法将 inflate 视图和绑定合成一步。这里有两个方法重载,一个只需要提供LayoutInflater,另外一个还需要 ViewGroup 参数:

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

如果布局的 inflated 使用了其他的机制,可以进行单独绑定:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

一些情况下 binding 类提前不知道,在这种情况下可以通过 DataBindingUtil 创建 binding 对象:

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

带有ID的View

Binding 类会为每个带有ID的view 生成名为ID的 public final 属性,Binding 类会在构造方法中会遍历一次View的层次结构以初始化这些 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>

在 binding 类中会生成这样的属性:

public final TextView firstName;
public final TextView lastName;

在使用 databingding 的情况下一般是不需要设置ID的,但是还是存在一些情况需要通过代码获取View的。

变量

Databinding 会为每个变量生成 getter 和 setter 方法。

<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 类中会生成如下的代码:

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 有些不同,在被设置成可见或显示的 inflate 之前是不可见的,在布局中她会通过 inflating 另外一个布局 替换自身。

在 binding 类中会生成一个 final 的 ViewStubProxy 而不是ViewStub,开发者可以通过getViewStub来获取 ViewStub将它 inflate( inflate 之后 getViewStub 将返回 null )。

当 inflate 另外一个布局的时候必须建立一个新的 binding ,因此ViewStubProxy 必须监听 ViewStub.OnInflateListener ,并初始化 binding ,因为ViewStub 只能设置一个OnInflateListener所以ViewStubProxy会保存一个OnInflateListener(提供 set 方法)并在ViewStub.OnInflateListener回调中调用OnInflateListener

高级绑定

动态变量

有时,我们不知道具体的 binding 类。例如RecyclerView.Adapter中存在多个布局文件时,我们是不知道具体的 binding 类的。但是仍然需要在onBindViewHolder(VH, int)方法中设置 binding 的变量。

在这个例子中,所有布局中都含有一个“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();
}

 ##### 立即绑定 当一个变量或者 observable 改变的时候, binding 会在下一帧执行这些改变。这会有一些时间的间隔,如果你想立刻执行这些改变可以调用 executePendingBindings()方法。

后台线程

只要你的数据不是一个集合你就可以在后台线程更新这个数据,Data binding将会在UI线程更新View的状态,另外 Data binding 还会缓存这些变量和字段来避免并发问题。

Attribute Setters


无论何时绑定的数据改变了,binding 类就必须调用View上的一个setter方法。data binding 框架提供了一些自定义的方法来关联属性和View上的方法。

Automatic Setters

对于一个 View 的属性,data binding 会尽力去寻找它相应的设置方法。属性的命名空间并不重要,重要的是属性的名字。例如,一个表达式设置给了 TextView 的android:text属性,将查找 TextView 的 setText(String)。如果表达式返回一个整型值则会查找 setText(int) 方法。一定要注意表达式的返回值并在必要时做类型转换。注意即使你使用了一个没有的属性 data binding 也会正常工作。你可以为View上的任意setter方法创造一个属性。例如,support 库的 DrawerLayout 没有任何属性但是有很多的 set 方法,你可以通过 Automatic Setters 来调用这些方法。

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

重命名 Setters

有一些属性和它对应的 setter 方法是不匹配的。对于这些方法可以使用 BindingMethods注解修改属性和 setter 方法的对应关系。必须存在一个包含 BindingMethod注解的类,它会改变所有属性和 setter 方法的关联关系。例如android:tint实际上和 setImageTintList(ColorStateList)方法相关联的而不是setTint

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

自定义 Setters

一些属性需要自定义 binding 的逻辑。例如,没有一个相关的 setter 方法和android:paddingLeft属性相对应,只有setPadding(left, top, right, bottom)方法。开发者可以在 静态 binding adapter 方法上添加 BindingAdapter 注解来自定义一个属性。

binding 库已经提供了一些BindingAdapters定义的属性,paddingLeft就是其中之一:

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

Binding adapters 对于其他类型的自定义是很有用的,例如,一个自定义的 loader 可以用来关闭加载图片的线程。

当开发者的 binding adapters 和默认的 adapters 冲突时 默认的 adapters 会被覆盖。

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

imageUrlerror 同是出现在 ImageView 中的时候这个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());
   }
}

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

@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 包含多个方法时,它就必须被拆分成包含单个方法的多个接口。例如 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 也会影响另外一个,所以必须创建3个方法:

@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 方法而不是set方法。android.databinding.adapters.ListenerUtil工具类帮助追踪前一个listeners(弱引用的方式)所以它有可能已经被删除了。

通过在OnViewDetachedFromWindowOnViewAttachedToWindow上面添加@TargetApi(VERSION_CODES.HONEYCOMB_MR1)注解,告诉 data binding 这些代码只能运行在Honeycomb MR1或以后的版本中。 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)上的注解也是这样。

类型转换


对象转换

当 binding 表达式返回一个对象时,就会调用一个 setter(上面介绍的三种方式中匹配) 。这个对象将作为参数传递给这个 setter 方法。

使用ObservableMaps可以很方便的持有一个数据。例如:

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

userMap返回的对象将被自动转换为setText(CharSequence)方法的参数。当无法自动转换时需要开发者在表达式中显示转换。

自定义转换

有时一些特殊的类型需要自动的转换。例如设置View的背景:

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

View 的背景需要一个Drawable,但是表达式返回的是一个整型的颜色值。需要将 int值转换成ColorDrawable。下面的静态方法实现了这个功能:

@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对 Data Binding 的支持

Android Studio 支持 Data Binding 的许多代码编辑特性。 data binding 表达式的如下特性:

  • 代码高亮显示
  • 标记语言表达语法错误
  • XML代码补全
  • References, including navigation (such as navigate to a declaration) and quick documentation

注意 数组 和 泛型,例如  Observable 可能会误报错误。

如果在 data binding 表达式中使用了默认值 它会在预览界面显示出来。就象下面的例子中一样,预览界面会显示 TextView 的默认值PLACEHOLDER

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

在设计阶段也可以使用 tools 属性来显示默认值,详见Designtime Layout Attributes