1. DataBinding - offical tutorial

1. DataBinding - offical tutorial

该文档讲述了如何使用 Data Binding 库来编写申明式的布局,在构建应用逻辑和布局时,如何最少化代码。

该库非常灵活,并且兼容性好。它是一个 support 库,因此,在 Android 2.1(API level 7) 及以上的版本上,都可以使用它。

Gradle Android Plugin 的版本要求是 1.5.0-alpha1 及以上。

构建环境


modulebuild.gradle 文件中添加如下的配置:

  1. android { 
  2. .... 
  3. dataBinding { 
  4. enabled = true 


如果应用的某个 module 依赖了使用 data binding 的库,那么,该 module 也必须在自己的 build.gradle 文件中进行上述的配置。

当然,Android Studio 的版本也得 1.3 及以上才行。

数据与布局文件的绑定

data binding 表达式


data binding 布局文件与正常的布局文件大不相同,它的根节点是 layout 元素,layout 中的两个直接子元素是:dataviewview 元素即是非 data binding 的布局文件中的任意根节点:

  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android"> 
  3. <data> 
  4. <variable name="user" type="com.example.User"/> 
  5. </data> 
  6. <LinearLayout 
  7. android:orientation="vertical" 
  8. android:layout_width="match_parent" 
  9. android:layout_height="match_parent"> 
  10. <TextView android:layout_width="wrap_content" 
  11. android:layout_height="wrap_content" 
  12. android:text="@{user.firstName}"/> 
  13. <TextView android:layout_width="wrap_content" 
  14. android:layout_height="wrap_content" 
  15. android:text="@{user.lastName}"/> 
  16. </LinearLayout> 
  17. </layout> 

data 节点中的 user 变量描述了可能会在布局中用到的变量:

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

布局中,使用 @{} 表达式来为属性赋值。下面的代码中,把 User 对象的 firstName 属性的值赋给了 TextView

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

数据对象


现在,假定你已经有了 User 对象的 POJO,如下:

  1. public class User
  2. public final String firstName; 
  3. public final String lastName; 
  4. public User(String firstName, String lastName)
  5. this.firstName = firstName; 
  6. this.lastName = lastName; 


这样对象的数据永远不会改变。在应用中,这种情况通常是数量一旦被读取,就不能再改变了。因此,可以使用 JavaBean 对象:

  1. public class User
  2. private final String firstName; 
  3. private final String lastName; 
  4. public User(String firstName, String lastName)
  5. this.firstName = firstName; 
  6. this.lastName = lastName; 

  7. public String getFirstName()
  8. return this.firstName; 

  9. public String getLastName()
  10. return this.lastName; 


对于 data binding 来说,这两个类是等价的。TextView 控件的值使用了 @{user.firstName} 表达式作为 android:text 属性的值,对于前一个类来说,该表达式将获取 firstName 变量的值,对于后一个类来说,该表达式将获取 getFirstName() 方法的值。如果 firstName() 方法存在,作为一种选择,表达式也可以从这个方法中获取值。

绑定数据


默认地,会基于布局文件的名称自动生成绑定类,转换规则是 Pascal 并在名称添加后缀:Binding。上面示例中,布局文件的名称是:main_activity.xlm,因此,自动生成的绑定类就是:MainActivityBinding。绑定类将会持有所有从布局文件(例如:user 这个变量)到视图绑定的引用,这样,系统才能知道如何给绑定表达式赋值。这就意味着,在 inflating 时创建绑定是一件非常容易的事情:

  1. @Override 
  2. protected void onCreate(Bundle savedInstanceState)
  3. super.onCreate(savedInstanceState); 
  4. MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity); 
  5. User user = new User("Test", "User"); 
  6. binding.setUser(user); 

上面的代码就完成了所有的工作。运行应用,你将在界面上看到 User 对象的相关信息。下面的代码也可以完成 inflating 操作:

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

如果,你正在 ListViewRecycleView 的适配器中为 item 使用数据绑定,那你应该这样写:

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

事件处理


data binding 可以写事件处理的表达式,并把它分发给 view。事件属性名称由监听方法的名称决定,但是有一些变化。例如:View.onLongClickListener 所对应的事件方法就是 onLongClick(),因此,该事件对应的属性就是:android: onLongClick。要处理一个事件,有两种方法:

  • 方法引用:在表达式中,可以引用与监听方法签名一致的方法。当一个表达式被当作一个方法引用时,data binding 会在一个监听器中把方法引用和宿主对象包裹起来,然后把该监听设置给目标 View。如果表达式最终的值为 null,那么,data binding 就不会创建监听,而是设置一个 null 监听。

  • 监听绑定:data binding 总是创建监听,并把它设置给 view。当事件被分发,监听转换成 lambda 表达式。

方法引用


事件可以直接绑定到处理方法上,与 android:onClick 的属性值可以指定为 Activity 中的某个方法类似。与 View#onClick 属性相比,这种方法的主要优点就是:在编译的时候,表达式就被处理了。因此,如果方法不存在或者方法签名不正确,在编译时期就能知道了。

方法引用和监听绑定最大的不同在于:当数据绑定时,就会创建真正的监听实现,而不是事件触发时才创建。如果你更喜欢在事件发生时使用表达式,那你应该使用监听绑定。

要给一个事件指定处理方法,需要使用绑定表达式,调用方法的名称就可以了。例如:

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

绑定表达式可以把点击的监听指定给某个 View

  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android"> 
  3. <data> 
  4. <variable name="handlers" type="com.example.Handlers"/> 
  5. <variable name="user" type="com.example.User"/> 
  6. </data> 
  7. <LinearLayout 
  8. android:orientation="vertical" 
  9. android:layout_width="match_parent" 
  10. android:layout_height="match_parent"> 
  11. <TextView android:layout_width="wrap_content" 
  12. android:layout_height="wrap_content" 
  13. android:text="@{user.firstName}" 
  14. android:onClick="@{handlers::onClickFriend}"/> 
  15. </LinearLayout> 
  16. </layout> 

注意:表达式中方法的签名必须与原方法签名完全一致。

监听绑定


监听绑定就是绑定当事件发生时运行的表达式。它与方法引用类似,但是,它可以运行任意的 data binding 表达式。该特性在 Android Gradle Plugin for Gradle 2.0 及以上版本中可用。

在方法引用中,方法的参数必须完全匹配事件监听方法的参数。但是在监听绑定中,只要方法的返回值与监听的返回值一致就可以了(除非是 void)。例如:

  1. public class Presenter
  2. public void onSaveClick(Task task){} 

绑定:

  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android"> 
  3. <data> 
  4. <variable name="task" type="com.android.example.Task" /> 
  5. <variable name="presenter" type="com.android.example.Presenter" /> 
  6. </data> 
  7. <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> 
  8. <Button android:layout_width="wrap_content" android:layout_height="wrap_content" 
  9. android:onClick="@{() -> presenter.onSaveClick(task)}" /> 
  10. </LinearLayout> 
  11. </layout> 

监听采用了 lambda 的写法,只有当它是表达式的根元素时才可以。如果表达式中存在回调,data binding 会自动创建这些监听,并为事件注册它们。当 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 参数。对于监听的参数,监听绑定提供了两种选择:要么完全忽略所有参数,要么提供参数的名称。如果想使用参数的名称,可以直接在表达式中写。例如:

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

如果想在方法中使用参数,应该这样写:

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

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

lambda 表达式也支持多个参数:

  1. public class Presenter
  2. public void onCompletedChanged(Task task, boolean completed){} 

  1. <CheckBox android:layout_width="wrap_content"  
  2. android:layout_height="wrap_content"  
  3. android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" /> 

如果监听事件的返回值不是 void,那么,表达式也必须返回同样的值。例如,如果要监听长按事件,那么,表达式也应该返回 boolean 类型:

  1. public class Presenter
  2. public boolean onLongClick(View view, Task task){} 

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

如果表达式由于 null 对象而不能转换,data binding 将会返回该对象默认的值。例如,对象是 nullint0booleanfalse 等。

如果在表达式中需要用到判断(例如三元运算符),可以把 void 当作符号使用:

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

布局细节

import


data 节点中,可以使用一个或多个 importimport 语句使得在布局文件中使用 Java 类成为可能,与 Java 类中的使用一样:

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

经过上述的定义,View 类就可以在表达式中使用了:

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

如果同一个布局文件中有相同的类名发生混淆,可以使用 alias 属性为类设置别名:

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

经过上述设定,Vista 就代表着 com.example.real.estate.ViewView 代表着 android.view.View。引入的类型可以在 variable 或表达式中使用:

  1. <data> 
  2. <import type="com.example.User"/> 
  3. <import type="java.util.List"/> 
  4. <variable name="user" type="User"/> 
  5. <variable name="userList" type="List&lt;User&gt;"/> 
  6. </data> 

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

表达式中,还可以使用引入的类中的静态变量和方法:

  1. <data> 
  2. <import type="com.example.MyStringUtils"/> 
  3. <variable name="user" type="com.example.User"/> 
  4. </data> 
  5. … 
  6. <TextView 
  7. android:text="@{MyStringUtils.capitalize(user.lastName)}" 
  8. android:layout_width="wrap_content" 
  9. android:layout_height="wrap_content"/> 

注意:java.lang.* 会被自动导入·

variable


data 中,可以使用任意数量的 variablevariable 用于描述属性,在 layout 中的属性可以在布局文件的表达式中使用。

  1. <data> 
  2. <import type="android.graphics.drawable.Drawable"/> 
  3. <variable name="user" type="com.example.User"/> 
  4. <variable name="image" type="Drawable"/> 
  5. <variable name="note" type="String"/> 
  6. </data> 

变量类型在编译时进行检查,因此,如果变量实现了 Observable 或者是一个 observable colletion,将会使用类型进行反射。如果变量是一个没有实现 Observable* 的基本类或接口,那么,变量将不会被观察

如果对于不同的配置(例如:横竖屏)采用了不同的布局文件,那么,变量将会合二为一。在这样的布局文件中,变量的定义一定不能冲突。

对于每一个申明的变量来说,在生成的绑定类中,都会为它们生成 gettersetter 方法。在 setter 被调用之前,变量的值是对应 Java 类型的默认值 -- 对象是 nullint0booleanfalse 等。

在表达式中,如果有需要,会生成名为 context 的特殊变量,它的值来源于根 ViewgetContext() 方法。如果在布局文件中申明了同样名称的变量,该变量将会被重写。

自定义绑定类的名称


默认地,绑定类是根据布局文件的名称生成,以大写字母开始,去掉下划线(_)并把每个单词的首字母大写,最后添加后缀 Binding。这些自动生成的绑定类将被放置于模块下的 databinding 包内。例如,名为 contact_item.xml 的布局文件将生成名为 ContactItemBinding 的文件。如果模块的包名为 com.example.my.app,那么,绑定文件将放置于 com.example.my.app.databinding 包下。

使用 data 元素的 class 属性,可以修改绑定类生成的名称和放置的地方,例如:

  1. <data class="ContactItem"> 
  2. ... 
  3. </data> 

上面示例的布局文件生成的绑定类名为:ContactItem,放置的地方是模块下的 databinding 包。如果要放在不同的包,请加前缀 .

  1. <data class=".ContactItem"> 
  2. ... 
  3. </data> 

在这种情况下,ContactItem 类将直接放置于模块的包下。如果提供了完整的包路径,那么可以使用任意的路径:

  1. <data class="com.example.ContactItem"> 
  2. ... 
  3. </data> 

include


通过应用的全名空间和属性名,变量可以从容器布局中传递到被包含的布局中:

  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android" 
  3. xmlns:bind="http://schemas.android.com/apk/res-auto"> 
  4. <data> 
  5. <variable name="user" type="com.example.User"/> 
  6. </data> 
  7. <LinearLayout 
  8. android:orientation="vertical" 
  9. android:layout_width="match_parent" 
  10. android:layout_height="match_parent"> 
  11. <include layout="@layout/name" 
  12. bind:user="@{user}"/> 
  13. <include layout="@layout/contact" 
  14. bind:user="@{user}"/> 
  15. </LinearLayout> 
  16. </layout> 

从上面的代码中可知,name.xmlcontact.xml 文件中,必须得有名为 user 的变量。

data binding 不支持根元素为 merge。例如,下面是错误的示例

  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android" 
  3. xmlns:bind="http://schemas.android.com/apk/res-auto"> 
  4. <data> 
  5. <variable name="user" type="com.example.User"/> 
  6. </data> 
  7. <merge> 
  8. <include layout="@layout/name" 
  9. bind:user="@{user}"/> 
  10. <include layout="@layout/contact" 
  11. bind:user="@{user}"/> 
  12. </merge> 
  13. </layout> 

表达式

一般特性


表达式看起来与 Java 表达式很类似,下面是相同部分:

  • 数学计算:+ - * / %

  • 字符连接:+

  • 逻辑运算:&& ||

  • 二进制运算:& | ^

  • 一元运算符:+ - ! ~

  • 位运算:>> >>> <<

  • 比较运算符:== > < >= <=

  • instanceof

  • 分组:()

  • 字面值:character String numberic null

  • 计算

  • 方法调用

  • 字段获取

  • 数组元素获取

  • 三元运算符:?:

示例:

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

缺失的操作符


相比能在 Java 中使用的语法来说,表达式缺失了少量的操作符:

  • this

  • super

  • new

  • EXplicit generic invocation

null 的合并操作符


该操作符的意思是:如果左边的表达式不为 null,最后的表达式的值为左边表达式的值。如果为 null,是右边表达式的值。

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

上面示例代码的功能相当于下面代码的功能:

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

属性引用


该形式在一开始就讨论过了:JavaBean 引用的短格式:

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

避免 NullPointerException


生成的数据绑定代码会自动检查 null 来避免空指针异常。例如,@{user.name},如果 usernulluser.name 将被指定为默认值(null)。如果引用了 user.ageage 的类型是 int,那么默认值就是 0

集合


常用的集合:arraylistsparse listmap,要获取值,使用 [] 操作符就行,非常方便。

  1. <data> 
  2. <import type="android.util.SparseArray"/> 
  3. <import type="java.util.Map"/> 
  4. <import type="java.util.List"/> 
  5. <variable name="list" type="List&lt;String&gt;"/> 
  6. <variable name="sparse" type="SparseArray&lt;String&gt;"/> 
  7. <variable name="map" type="Map&lt;String, String&gt;"/> 
  8. <variable name="index" type="int"/> 
  9. <variable name="key" type="String"/> 
  10. </data> 
  11. … 
  12. android:text="@{list[index]}" 
  13. … 
  14. android:text="@{sparse[index]}" 
  15. … 
  16. android:text="@{map[key]}" 

字符字面值


如果对属性值使用了单引号,那么表达式就可以使用双引号了:

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

当然,可以对属性值使用双绰号,这时,字符字面值就应该使用 ` 符号:

  1. android:text="@{map[`firstName`}" 
  2. android:text="@{map['firstName']}" 

资源


使用正常的语法就可以在表达式中获取资源:

  1. android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" 

stringplurals 来说,也可以提供参数对其进行格式化:

  1. android:text="@{@string/nameFormat(firstName, lastName)}" 
  2. android:text="@{@plurals/banana(bananaCount)}" 

如果 plural 有多个参数,那么所有的参数都应该被传递:

  1. Have an orange 
  2. Have %d oranges 
  3.  
  4. 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 都可以用于数据绑定,但是,修改 POJO 不会引起 UI 的更新。数据绑定的真正强大之处在于:数据变化了,有能力通知数据对象。数据变化通知机制有三种:Observable objectObservable fieldObservable collection

当这三种可观察数据对象中的某一种类型的数据绑定到 UI 时,数据对象的属性发生了变化,UI 将会被自动更新。

Observable 对象


实现了 Observable 接口的类允许绑定附着一个监听,把对象和监听绑定到一起,用于观察对象所有属性的变化。

Observable 接口有添加和移除监听的机制,但是,通知却是取决于开发者。为了使开发简单,创建了一个实现了监听注册机制的基本类:BaseObservable。当属性变化时,数据实现类仍然会响应通知。要实现这样的效果,需要在 getter 方法上添加 @Bindable 注解,并在 setter 方法中进行通知:

  1. private static class User extends BaseObservable
  2. private String firstName; 
  3. private String lastName; 
  4. @Bindable 
  5. public String getFirstName()
  6. return this.firstName; 

  7. @Bindable 
  8. public String getLastName()
  9. return this.lastName; 

  10. public void setFirstName(String firstName)
  11. this.firstName = firstName; 
  12. notifyPropertyChanged(BR.firstName); 

  13. public void setLastName(String lastName)
  14. this.lastName = lastName; 
  15. notifyPropertyChanged(BR.lastName); 


在编译期,@Bindable 注解会在 BR 类(该类的模块的包下)中生成一个实体。如果数据类的基类不能被改变,那么,也可以使用便捷的 PropertyChangeRegistery 来实现 Observable 接口,有效的存储和通知监听。

ObservableField


在创建 Observable 类时,会引入一些工作量,因此,想节约时间的开发者、或拥有少量属性的类可以使用 ObservableField 类型。它有些同辈:ObservableBooleanObservableByteObservableCharObservableShortObservableIntObservableLongObservableFloatObservableDoubleObservableParcelable

  1. private static class User
  2. public final ObservableField<String> firstName = 
  3. new ObservableField<>(); 
  4. public final ObservableField<String> lastName = 
  5. new ObservableField<>(); 
  6. public final ObservableInt age = new ObservableInt(); 

要访问变量的值,应该使用 setget 方法:

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

Observable 集合


应用通常使用一些动态的结构来存储数据。Observable 集合可以通过 key 值获取数据对象。当 key 是一个引入类型时,ObservableArrayMap 类型非常有用,例如 String

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

布局文件中,可以通过 key 获取到值:

  1. <data> 
  2. <import type="android.databinding.ObservableMap"/> 
  3. <variable name="user" type="ObservableMap&lt;String, Object&gt;"/> 
  4. </data> 
  5. … 
  6. <TextView 
  7. android:text='@{user["lastName"]}' 
  8. android:layout_width="wrap_content" 
  9. android:layout_height="wrap_content"/> 
  10. <TextView 
  11. android:text='@{String.valueOf(1 + (Integer)user["age"])}' 
  12. android:layout_width="wrap_content" 
  13. android:layout_height="wrap_content"/> 

key 是整形时,ObservableArrayList 类型非常有用:

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

布局文件中,通过标识获取列表:

  1. <data> 
  2. <import type="android.databinding.ObservableList"/> 
  3. <import type="com.example.my.app.Fields"/> 
  4. <variable name="user" type="ObservableList&lt;Object&gt;"/> 
  5. </data> 
  6. … 
  7. <TextView 
  8. android:text='@{user[Fields.LAST_NAME]}' 
  9. android:layout_width="wrap_content" 
  10. android:layout_height="wrap_content"/> 
  11. <TextView 
  12. android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}' 
  13. android:layout_width="wrap_content" 
  14. android:layout_height="wrap_content"/> 

生成绑定


自动生成的绑定类链接了是布局文件中 View 与布局 variable 之间的桥梁。正如前期讨论的那样,绑定类的名称和存放位置可以被自定义。所有生成的绑定类都继承于 ViewDataBinding

创建


inflation 之后,绑定会立即创建,这样可以确保:视图层次不会先于视图与表达式绑定动作之前发生。找到绑定布局有几种方式,最常用的就是使用绑定类的静态方法。inflate 方法会一次性渲染视图层次并进行绑定:

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

如果布局已经使用其它的机制进行了渲染,那就单独进行绑定:

  1. MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot); 

有时候,绑定不能被编译器识别,这时就得使用 DataBindingUtil 类:

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

带 ID 的 View


将会为布局文件中每个带有 IDView 生成类型为 public final 的字段。绑定其实做了一个 View 层次方面的单向传递,它会抽取 ViewID。该机制的速度比调用 findViewById 方法的速度更快,例如:

  1. <layout xmlns:android="http://schemas.android.com/apk/res/android"> 
  2. <data> 
  3. <variable name="user" type="com.example.User"/> 
  4. </data> 
  5. <LinearLayout 
  6. android:orientation="vertical" 
  7. android:layout_width="match_parent" 
  8. android:layout_height="match_parent"> 
  9. <TextView android:layout_width="wrap_content" 
  10. android:layout_height="wrap_content" 
  11. android:text="@{user.firstName}" 
  12. android:id="@+id/firstName"/> 
  13. <TextView android:layout_width="wrap_content" 
  14. android:layout_height="wrap_content" 
  15. android:text="@{user.lastName}" 
  16. android:id="@+id/lastName"/> 
  17. </LinearLayout> 
  18. </layout> 

上述代码的自动生成类中,将包含如下两个字段:

  1. public final TextView firstName; 
  2. public final TextView lastName; 

不使用数据绑定,ID 不是必须的,但是数据绑定中使用 ID,可以在代码中获取 View

变量


绑定将会自动为每个变量生成 accessor 方法:

  1. <data> 
  2. <import type="android.graphics.drawable.Drawable"/> 
  3. <variable name="user" type="com.example.User"/> 
  4. <variable name="image" type="Drawable"/> 
  5. <variable name="note" type="String"/> 
  6. </data> 

上述的代码将在绑定类中生成如下的代码:

  1. public abstract com.example.User getUser()
  2. public abstract void setUser(com.example.User user)
  3. public abstract Drawable getImage()
  4. public abstract void setImage(Drawable image)
  5. public abstract String getNote()
  6. public abstract void setNote(String note)

ViewStub


ViewStub 与正常的 View 略微不同。它会一直不可见,除非把它设置为可见,或者显式地渲染它。

因为从本质上说,ViewStub 不会出现在 View 层次上,the View in the binding object must also disappear to allow collection. Because the Views are final, a ViewStubProxy object takes the place of the ViewStub, giving the developer access to the ViewStub when it exists and also access to the inflated View hierarchy when the ViewStub has been inflated.

When inflating another layout, a binding must be established for the new layout. Therefore, the ViewStubProxy must listen to the ViewStub's ViewStub.OnInflateListener and establish the binding at that time. Since only one can exist, the ViewStubProxy allows the developer to set an OnInflateListener on it that it will call after establishing the binding.

高级绑定

动态变量


有时,某个绑定总是不能被识别。例如,操作任意布局的 RecycleView.Adapter 将不能识别特定的绑定类。只有当 onBindViewHolder(VH, int) 时,才会指定绑定的值。

在这种情况下,RecycleView.Adapter 绑定的所有布局都有一个 item 变量。BindingHoldergetBinding 方法将返回基本的 ViewDataBinding 类:

  1. public void onBindViewHolder(BindingHolder holder, int position)
  2. final T item = mItems.get(position); 
  3. holder.getBinding().setVariable(BR.item, item); 
  4. holder.getBinding().executePendingBindings(); 

即时绑定


当变量或可观察者改变时,在下一个 frame 之前,绑定将会按计划进行改变。但是有时候,绑定需要立即被执行。这时,就需要使用 executePendingBindings() 方法。

后台线程


只要数据模型不是集合,就可以在后台对数据模型进行改变。在进行这样操作的时候,数据绑定会本地化每个变量和字段,这样可以避免任何迸发性的问题。

属性 setter 方法


每当绑定的值发生了变化,View 的表达式如果使用了变量,自动生成的绑定类必须调用 setter 方法。

自动生成的 settter


对于任一属性来说,数据绑定会尝试寻找设置属性的方法。属性的全名空间没有关系,仅仅与属性名称有关。例如,TextView 控件的 android:text 属性使用了表达式,那将会查找 setText(String) 这个方法。如果表达式返回的是整形,那么数据绑定将会查找 setTExt(int) 这个方法。务必得让表达式返回正确的类型,如果有需要,对返回值进行类型转换。注意:即使属性名称不存在,数据绑定也会进行。通过数据绑定,很容易就可以为任何 setter “创建”属性。例如,支持库的 DrawerLayout 没有任何的属性,但是有大量的 setter。你就可以使用这些自动 setter 方法中的任意一个:

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

对 setter 进行重命名


某些属性的 setter 方法与属性名称并不匹配。对于方法来说,可以使用 BindingMethods 注解来给属性指定 setter 方法。该注解得写在类上,类中还得包含使用 BindingMethod 注解的方法。例如,下面代码中的 android:tint 属性与 setImageTintList(ColorStateList) 相关联,而不是与 setTint 方法关联。

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

开发者对 setter 进行重命名的情况不多,因为 Android 框架已经实现了系统属性的 setter

自定义 setter


某些属性需要自定义绑定逻辑。例如,android:paddingLeft 属性没有相关的 setter 方法。但是,setPadding(left, top, right, bottom) 方法存在。开发者可以使用拥有 BindingAdapter 注解的静态方法来自定义某个属性调用的 setter 方法。

  1. @BindingAdapter("android:paddingLeft"
  2. public static void setPaddingLeft(View view, int padding)
  3. view.setPadding(padding, 
  4. view.getPaddingTop(), 
  5. view.getPaddingRight(), 
  6. view.getPaddingBottom()); 

绑定适配器对于自定义的类型非常有用。例如,自定义的 loader 可以在非主线程上加载图片。

如果发生了冲突,开发者自定义的绑定适配器会覆盖默认的适配器。

接收多个参数的适配器写法如下:

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

  1. <ImageView app:imageUrl="@{venue.imageUrl}" 
  2. app:error="@{@drawable/venueError}"/> 

上面的适配器在 imageUrlerror 上都会被调用,并且一个类型是 String,一个类型是 Drawable

  • 自定义命名空间将被忽略

  • 可以在 Android 命名空间下写适配器

绑定适配器在处理中,也可以使用旧值。一个方法如果既有新值又有旧值,应该先旧值,后新值:

  1. @BindingAdapter("android:paddingLeft"
  2. public static void setPaddingLeft(View view, int oldPadding, int newPadding)
  3. if (oldPadding != newPadding) { 
  4. view.setPadding(newPadding, 
  5. view.getPaddingTop(), 
  6. view.getPaddingRight(), 
  7. view.getPaddingBottom()); 


只能在接口或有抽象方法的抽象类中使用事件处理,例如:

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

  7. if (newValue != null) { 
  8. view.addOnLayoutChangeListener(newValue); 



如果一个监听有多个方法,那么它必须被拆分为多个监听。例如,View.OnAttachStateChangeListener 有两个方法:onViewAttachedToWindow()onViewDetachedFromWindow()。因此,必须定义两个接口来区分和处理:

  1. @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 
  2. public interface OnViewDetachedFromWindow
  3. void onViewDetachedFromWindow(View v)

  4.  
  5. @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 
  6. public interface OnViewAttachedToWindow
  7. void onViewAttachedToWindow(View v)

因为一个接口的改变会影响另外一个接口,因此,必须使用三个不同的绑定适配器,每个属性一个,然后两个合起来一个:

  1. @BindingAdapter("android:onViewAttachedToWindow"
  2. public static void setListener(View view, OnViewAttachedToWindow attached)
  3. setListener(view, null, attached); 

  4.  
  5. @BindingAdapter("android:onViewDetachedFromWindow"
  6. public static void setListener(View view, OnViewDetachedFromWindow detached)
  7. setListener(view, detached, null); 

  8.  
  9. @BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}) 
  10. public static void setListener(View view, final OnViewDetachedFromWindow detach, 
  11. final OnViewAttachedToWindow attach)
  12. if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) { 
  13. final OnAttachStateChangeListener newListener; 
  14. if (detach == null && attach == null) { 
  15. newListener = null
  16. } else
  17. newListener = new OnAttachStateChangeListener() { 
  18. @Override 
  19. public void onViewAttachedToWindow(View v)
  20. if (attach != null) { 
  21. attach.onViewAttachedToWindow(v); 


  22.  
  23. @Override 
  24. public void onViewDetachedFromWindow(View v)
  25. if (detach != null) { 
  26. detach.onViewDetachedFromWindow(v); 


  27. }; 

  28. final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, 
  29. newListener, R.id.onAttachStateChangeListener); 
  30. if (oldListener != null) { 
  31. view.removeOnAttachStateChangeListener(oldListener); 

  32. if (newListener != null) { 
  33. view.addOnAttachStateChangeListener(newListener); 



上面的示例稍稍有些难懂,这是因为 View 单独使用添加或移除的监听,而不是集合方法:View.OnAttachStateChangeListenerandroid.databinding.adapters.ListenerUtil 会保持前置监听的跟踪,因此,就可以从适配器中适配它们了。

上面的方法添加了 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 注解,这是用于告诉数据绑定代码生成器:只有程序运行在 Honeycomb MR1 及以上版本的设备上时,才会生成这些监听。因为只有这些设备,才支持 addOnAttachStateChangeListener(View.OnAttachStateChangeListener) 方法。

转换器

对象转换


如果绑定表达式返回的是对象,那么,将会从 自动生成的、重命名的、自定义的 setter 中查找合适的 setter。最终,对象将被解析为所选择 setter 的参数类型。

对于使用 ObservableMap 类型存储数据的变量来说,下面的代码是便捷的获取方法:

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

上面的代码中,userMap 返回一个对象,生成器会找到 setText(String) 这个 setter 方法,并把这个对象进行解析。如果有参数类型可能发生混淆的情况存在,就需要开发者自己处理。

自定义转换


有时候,在指定的类型之间,转换应该自动进行。例如:

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

上面的代码中,background 属性需要 Drawable 类型,但是 colorint 类型。当期望值类型是 Drawable 而返回值类型是 int 时,int 值就会被转换为 ColorDrawable 类型。这个转换是在有 @BindingConversion 注解的静态方法中进行的:

  1. @BindingConversion 
  2. public static ColorDrawable convertColorToDrawable(int color)
  3. return new ColorDrawable(color); 

注意:转换只能发生在 setter 级别,并且,不允许混合类型。例如:

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

Android Studio 对数据绑定的支持


对于数据绑定代码来说,Android Studio 支持许多代码级的编辑特性。例如,支持数据绑定的如下特性:

  • 语法高亮

  • 表达式语法错误标识

  • xml 代码完成

  • 引用和快速文档

注意:Arraygeneric type (例如 Observable 类)有时可能会显示错误,尽管这里真的没有错误。

如果提供了表达式的默认值,预览面板将会显示该值:

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

在项目的设计阶段,如果需要显示默认值,可以使用工具,而不是使用默认值,在 这里 有介绍。

posted @ 2016-11-04 22:35  wchhuangya  阅读(623)  评论(0编辑  收藏  举报