Data Binding Library

2017.8.15 初次添加
2017.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的布局文件和普通的布局文件有一點不同,它的根布局是<layout>在根布局下面跟一個<data>元素和一個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或者 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort,ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容