Data Binding 使用指南

DataBinding 庫是 Google 公司 Android Framework UI 工具團隊開發出來的一款 Android 庫。DataBinding 庫增強了 Android 項目中的布局文件對 UI 的控制能力,以前對 UI 進行控制和響應 UI 發出的命令的代碼邏輯,現在就可以直接放在布局文件中使用 DataBinding 表達式 來表達,和 Java 代碼中的數據直接關聯。通過這種聲明式編程,可以大大減少 UI 和 邏輯代碼之間的“膠水代碼”,比如以前 Activity/Fragment 中的 UI 控制邏輯;從而增加了 layout xml 文件的表現力,當然也增加了 layout xml 文件的復雜程度,控制好 layout xml 文件中 bingding 表達式的復雜程度,把所有業務邏輯仍然放在 java 代碼中,是使用好 DataBinding 庫的關鍵。負責動畫控制的邏輯建議仍然放在 java 代碼中。
使用DataBinding庫以后你至少可以得到以下好處:

  1. 不用寫 setOnClickListener 之類的響應 UI 命令的代碼(響應 view 命令)
  2. 不用寫 setText() 之類的控制 view 屬性的代碼(控制 view)
  3. 不用寫 findviewbyid 代碼(2 的附加產物)

簡單一句話,DataBinding 讓你可以在布局文件中寫 java 表達式,所以可以省略掉中間層??梢哉f DataBinding 庫是減少甚至完全代替 view 和 業務邏輯 之間中間層 stupid code 的利器。

目錄

1. 搭建構建環境
2. Data Binding 中的布局文件入門
  2.1. 編寫 data binding 表達式
  2.2. 數據對象
  2.3. 綁定數據
  2.4. 事件處理
    2.4.1. 方法引用綁定
    2.4.2. Lisenter 綁定
    2.4.3. 避免復雜偵聽器
3. Data Binding 中的布局文件詳解
  3.1. import
  3.2. 變量
  3.3. 自定義 Binding 類名
  3.4. Includes
  3.5. 表達式語言
4. 數據對象
  4.1. Observable 對象
  4.2. Observable 屬性
  4.3. Observable 容器類
  4.3. 雙向綁定
5. 生成綁定
  5.1. 創建綁定
  5.2. 帶有 ID 的 View
  5.3. 變量
  5.4. ViewStub
  5.5. 高級綁定
    5.5.1. 動態綁定變量
    5.5.2. 立即 binding
    5.5.3. 后臺線程問題
6. 屬性 Setter
  6.1. 自動 Setter
  6.2. 重命名 Setter
  6.3. 自定義 Setter
7. 轉換器
  7.1. 對象轉換
  7.2. 自定義轉換
8. Android Studio 對 Data binding 的支持

這篇文檔介紹了如何使用 Data Binding Library 來編寫聲明式的布局,這樣可以減少應用中邏輯代碼和布局之間所需要的“膠水代碼”。

Data Binding Library 提供了非常好的靈活性與兼容性 - 它是一個 support library,可以在 Android 2.1(API level 7+)及其以上的平臺使用。

要使用 data binding,Android 的 Gradle 插件必須是 1.5.0-alpha1 或更高版本。

1. 搭建構建環境

  1. 在 Android SDK Manager 中下載最新的 Support Library。
  2. 在 app module 的 build.gradle 文件中添加 dataBinding 元素,如下:
android {
    ....
    dataBinding {
        enabled = true
    }
}

這樣DataBinding插件就會在你的項目內添加編譯和運行時必需的依賴配置。

如果你的 app module 依賴了一個使用 data binding 的庫,那么你的 app module 的 build.gradle 也必須配置 data binding

此外,您使用的 Android Studio 還要支持 DataBinding 特性才行。在 Android Studio 1.3 以及之后的版本提供了 data binding 的支持,詳見 Android Studio Support for Data Binding。

總結起來使用 DataBinding 環境要求如下:

  1. Android 2.1(API level 7+) 以上
  2. Android Gradle 插件 1.5.0-alpha1 以上
  3. 最新 Support Library
  4. Android Studio 1.3 以上

2. Data Binding 中的布局文件入門

2.1. 編寫 data binding 表達式

2.1.1 DataBinding 的布局文件與以前的布局文件有一點不同。它以一個 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>

2.1.2 在 data 標簽中定義的 user 變量,可以在布局中當作屬性來使用,用來寫一些和java代碼中表達式類似的"databinding表達式"

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

2.1.3 在布局文件中屬性值里使用 “@{}” 的語法,來表示"databinding表達式"。結合2.2,這里 TextView 的文本被設置為 user 中的 firstName 屬性。其中,user.firstName 和 java 中的表達式含義類似。

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

2.2. 數據對象

剛剛在布局文件中我們使用com.example.User類定義了一個 user 變量,現在我們假設 User 類是一個 plain-old Java object(POJO)。

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

因為成員變量都是final的,所以上面這個User類型的對象擁有不可改變的數據(immutable)。在應用中,這種寫一次之后永不變動數據的對象很常見。
這里也可以使用 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() 方法。此外,如果 user 中有 firstName() 方法存在,@{user.firstName}表達式也可以表示對firstName() 方法的調用。

2.3. 數據綁定

上面工作完成后,數據綁定工具在編譯時會基于布局文件生成一個 Binding 類。默認情況下,這個類的名字是基于布局文件的名字產生的,先把布局文件的名字轉換成帕斯卡命名形式,然后在名字后面接上”Binding”。例如,上面的那個布局文件叫 main_activity.xml,所以會生成一個 MainActivityBinding 類。這個類中包含了布局文件中所有的綁定關系,并且會根據綁定表達式給布局文件中的 View 屬性賦值(user變量和user表達式,view綁定,view數據綁定,view命令綁定)。編譯時產生Binding類主要完成了2個事情,1.解析layout文件,根據data標簽定義成員變量;2.解析layout文件,根據"databinding表達式"產生綁定代碼。Binding 創建好之后還需要,創建 Binding 類的對象,并和view綁定。

2.3.1 在 Activity 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);
}

就這么簡單!運行應用,你會發現測試信息已經顯示在界面中了。

DataBindingUtil.setContentView(this, R.layout.main_activity);

這句代碼主要做了3件事情,1.把布局設置給Activity,填充為view樹;2.創建Binding類對象;3.把view保存在Binding類的成員中,綁定view。這種方式只適合用于Activity中。

2.3.2 也可以通過下面這種方式綁定view:

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

MainActivityBinding.inflate() 會填充 MainActivityBinding 對應的布局,并創建 MainActivityBinding 對象,把布局和MainActivityBinding對象綁定起來。

2.3.3 如果在 ListView 或者 RecyclerView 的 adapter 中使用 data binding,可以這樣寫:

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

2.4. 命令綁定

上面演示了數據綁定,下面演示命令綁定,databinding 還允許你編寫表達式來處理view分發的事件(比如 onClick)。事件屬性名字取決于監聽器方法名字,例如View.OnLongClickListeneronLongClick()的方法,因此這個事件的屬性是android:onLongClick。

處理事件有兩種方法:

  • 方法綁定:在您的表達式中,您可以引用符合監聽器方法簽名的方法。當表達式的值為方法引用時,Data Binding會創建一個監聽器,并封裝方法引用和方法所有者對象,然后在目標視圖上設置該監聽器。如果表達式的值為null,DataBinding 則不會創建偵聽器,而是設置一個空偵聽器。
  • Lisenter 綁定:如果事件處理表達式中包含lambda表達式。DataBinding 會創建一個監聽器,設置給視圖。當事件分發時,偵聽器才會計算lambda表達式的值。

2.4.1. 方法綁定

事件可以直接綁定到事件處理器的方法上,類似于android:onClick可以分配一個 Activity 中的方法。與View#onClick屬性相比,方法綁定的主要優點是 DataBinding 表達式在編譯時就執行過了,因此如果該方法不存在或其簽名不正確,您會收到一個編譯時錯誤。

方法綁定和監聽器綁定之間的主要區別是,包裹方法引用的監聽器實現是在數據綁定時創建的,監聽器綁定是在觸發事件時創建的。如果您喜歡在事件發生時執行表達式,則應使用監聽器綁定。

如果想要將事件處理直接分配給處理程序,那就使用方法綁定表達式,該表達式值是要調用的方法名稱。例如,數據對象有如下方法:

public class MyHandlers {
    public void onClickFriend(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.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="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

注意,@{handlers::onClickFriend}表達式中onClickFriend的方法簽名必須與android:onClick監聽器對象中的方法簽名完全匹配。

2.4.2. Lisenter 綁定

Lisenter 綁定是在程序運行中事件發生時才綁定表達式。它和方法綁定類似,但 Listener 綁定允許運行時綁定的任意的數據表達式。此功能適用于版本2.0及更高版本的Android Gradle插件。 在方法綁定中,方法的參數必須與事件偵聽器的參數匹配。在 Listener 綁定中,只要返回值與 Lisenter 預期的返回值匹配就行(除非它期望void)

2.4.2.1 例如,您有一個 presenter 類,它具有以下方法:

public class Presenter {
    public void onSaveClick(Task task){}
}

然后,通過lambda表達式您可以將點擊事件綁定到您的類中,如下所示:

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

偵聽器只允許 DataBinding 表達式的根元素是lambda表達式。當在表達式中使用回調時,數據綁定自動創建必要的偵聽器并且為事件注冊。當視圖觸發事件時,數據綁定才執行給定的表達式。與在正則綁定表達式中一樣,在執行這些偵聽器表達式時,DataBinding 已經做好了空值和線程安全性的處理。

2.4.2.2 在上面的示例中,我們沒有在lambda表達式中定義傳遞給 onClick(android.view.View)方法的視圖參數。偵聽器綁定為偵聽器參數提供兩個選擇:1.忽略方法的所有參數;2.命名所有參數。

如果您喜歡命名參數,可以在表達式中使用它們。例如,上面的表達式可以寫成:

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

如果你想要使用表達式中的參數,可以在這樣使用:

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

您也可以使用具有多個參數的lambda表達式:

public class Presenter {
    public void onCompletedChanged(Task task, boolean completed){}
}
<CheckBox 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content"
    android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

如果正在偵聽的事件返回類型不是void類型的值,表達式也必須返回相同類型的值。例如,如果你想監聽長點擊事件,你的表達式應該返回布爾值。

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

如果由于空對象而無法計算表達式,數據綁定將返回該類型的默認Java值。例如,引用類型為null,int為0,boolean為false等。

2.4.2.3 如果需要使用帶謂詞(例如三元)的表達式,則可以使用void作為符號。

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
方法綁定和Lisenter綁定的區別.png

總結起來方法綁定和Listener綁定的區別如下

  • 方法引用綁定不能是表達式,Lisenter 綁定可以是表達式;
  • 方法引用綁定在綁定的時候會執行 DataBinding 表達式,可以自動處理空指針問題,Listener 綁定在事件觸發的時候才會執行 lambda 表達式;
  • 方法引用綁定會限制綁定的方法參數列表,返回值必須和監聽器中的方法一致,Listener 綁定只限制 lambda 表達式中語句的返回值和監聽器中的方法一致;
  • 方法引用不止能在 android:onClick= 這種命令綁定屬性中使用,在其他數據綁定屬性中也可以使用,而且可以使用表達式作為參數。
<TextView
    android:id="@+id/context_demo"
    android:text="@{user.load(context, @id/context_demo)}" />
public String load(Context context, int field) {
    return context.getResources().getString(R.string.app_name);
}

事件處理除了上述兩種方法,還可以直接以數據綁定的形式綁定一個監聽器對象(屬性setter小節講解)。

2.4.3. 避免復雜監聽器

Listener表達式非常強大,可以使代碼非常容易閱讀。另一方面,包含復雜表達式的 Listener 又會使布局難以閱讀和難以維護。這些表達式應該保持簡單,比如只用來從UI傳遞可用數據到回調方法一樣簡單,任何業務邏輯還是應該在從偵聽器表達式調用的回調方法中實現。

為了避免沖突,有些點擊事件處理程序他們需要一個專門的屬性,它們不是android:onClick。databinding已通過@BindingMethods注解(屬性setter小節講解)創建以下屬性來避免此類沖突。

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

3. Data Binding 中的布局文件詳解

3.1. import

3.1.1 data標簽內可以有0個或者多個 import 標簽。這樣你可以在布局文件中像在 Java 代碼中一樣引用這些類。

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

導入類以后,在databinding表達式中這個類就可以像在 java 代碼中一樣使用,比如用來定義變量,比如訪問類中的靜態方法和屬性。例如,現在 View 可以在表達式中如下面這樣引用:

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

3.1.2 當類名發生沖突時,還可以使用 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。

3.1.3 導入的類型可以用于變量的類型引用和表達式中:

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

3.1.4 導入的類后,就可以在表達式中使用類的靜態屬性/方法:

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

3.1.5 和 Java 一樣,java.lang.* 會被自動導入。

3.2. 變量

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 的特殊變量,這個變量可以被用于 binding 表達式中。context 變量其實是 rootView 的 getContext() 的返回值。如果在布局文件中自己定義 context 變量,默認的 context 變量會被自己定義的顯式 context 變量覆蓋。

3.3. 自定義 Binding 類名

默認情況下,binding 類的名稱取決于布局文件的命名,以大寫字母開頭,移除下劃線,后續字母大寫并追加 “Binding” 結尾。這個類會被放置在 databinding 包中。舉個例子,布局文件 contact_item.xml 會生成 ContactItemBinding 類。如果 module 包名為 com.example.my.app,binding 類會被放在 com.example.my.app.databinding 中。

3.3.1 通過設置 data 標簽中的 class 屬性,可以修改 Binding 類的命名與位置。比如:

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

會在 databinding 包中生成名為 ContactItem 的 binding 類。

3.3.2 如果需要放置在應用程序的包名下,可以在前面加 “.”,比如:

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

ContactItem 會直接生成在 module 包下。

3.3.3 如果提供完整的包名,binding 類可以放置在任何包名中,比如:

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

3.4. Includes

通過使用應用程序命名空間+變量名命名的屬性,布局中的變量可以從包含include標簽的布局中傳遞到 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>

xmlns:bind="http://schemas.android.com/apk/res-auto" bind 是應用程序命名空間,bind:user user 是變量名。另外需要注意,name.xml 與 contact.xml 中也都需要聲明 user 變量。比如 name.xml:

<?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:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textColor="@android:color/holo_red_dark"
            android:text="@{`include : ` + user.firstName}"/>
    </LinearLayout>

</layout>

上面布局中如果 user.firstName 值為 "zhao",那么 TextView 中就會顯示include:zhao

Data binding 不支持 merge 直接包含 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>

3.5. DataBinding表達式語言

3.5.1 通用特性

bingding 表達式語言與 Java 表達式有很多相似之處。下面是相同之處:

  • 數學運算 + - / * %
  • 字符串連接 +
  • 邏輯運算 && ||
  • 二進制運算 & | ^
  • 一元運算符 + - ! ~
  • 位移運算 >> >>> <<
  • 比較運算 == > < >= <=
  • instanceof
  • ()
  • 字面量 - 字符,字符串,數字,null
  • 類型轉換
  • 函數調用
  • 字段存取
  • 數組存取 []
  • 三元運算符 ?:

例如:

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

3.5.2 缺失的操作符

一些 Java 中的操作符在表達式語法中不能使用

  • this
  • super
  • new
  • 顯式泛型調用 <T>

3.5.3 Null合并運算符

Null合并運算符(??)會在非 null 的時候選擇左邊的操作,反之選擇右邊。

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

等同于

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

3.5.4 屬性引用

正如上面“編寫 data binding 表達式”中所討論的 JavaBean 的引用。在 DataBinding 表達式引用了一個對象的屬性時,它和訪問字段,getter,或者 ObservableFields使用相同的格式。

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

3.5.5 避免NullPointerException

自動生成的 data binding 代碼會自動檢查和避免 null pointer exceptions。舉個例子,在表達式 @{user.name} 中,如果 user 是 null,user.name 會賦予默認值 null。如果你引用了 user.age,因為 age 是 int 類型,所以默認賦值為 0。

3.5.6 容器類

通用的容器類:數組,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]}"

3.5.7 字符串字面量

當使用單引號包裹屬性時,就可以很簡單地在表達式中使用雙引號:

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

當使用雙引號包裹屬性時,字符串字面量就可以用雙引號轉義符號"或者單引號'或者反引號(`) 來包裹

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

3.5.8 資源

可以在 DataBinding 表達式中使用普通的語法來引用資源:

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

字符串格式化和復數形式字符串可以按下面這樣使用:
string.xml

<string name="firstname">FirstName: %1$s</string>
<string name="lastname">LastName: %1$s</string>
<plurals name="banana">
    <item quantity="zero">zero</item>
    <item quantity="one">one</item>
    <item quantity="other">other</item>
</plurals>

layout.xml

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

當復數形式并且有多個參數時,這樣使用:
strings.xml

<plurals name="numbers">
    <item quantity="one">Have an number</item>
    <item quantity="other">Have %1$d numbers</item>
</plurals>

layout.xml

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

4. 數據對象

任何 POJO 都能用在 data binding 中,但是更改 POJO 并不會同步更新 UI。DataBinding 的真正強大之處在于它可以讓你的數據對象擁有更新通知的能力。DataBinding 提供了三種的數據改變通知機制,Observable 對象observable 字段,與 observable 容器類
當上面的 observable 對象綁定在 UI 上,對象的屬性數據發生變化時,UI 就會同步更新。

4.1. Observable 對象

當一個類實現了 Observable 接口時,data binding 會設置一個 listener 綁定到的對象上,以便監聽對象字段的變動。

Observable 接口有添加/移除 listener 的機制,但發出通知還需要開發者來做。為了方便開發者,我們創建了一個基類 BaseObservable,它已經實現 listener 注冊機制,開發者只需要實現字段值變化的通知就行。

開發者只需要像下面例子一樣,1.在 getter 上使用 Bindable 注解,2.在 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 接口 + PropertyChangeRegistry 可以方便實現注冊 listener 并且通知數據改變。比如 BaseObservable 的實現:

public class BaseObservable implements Observable {
    private transient PropertyChangeRegistry mCallbacks;

    public BaseObservable() {
    }

    @Override
    public synchronized void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        if (mCallbacks == null) {
            mCallbacks = new PropertyChangeRegistry();
        }
        mCallbacks.add(callback);
    }

    @Override
    public synchronized void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        if (mCallbacks != null) {
            mCallbacks.remove(callback);
        }
    }

    /**
     * Notifies listeners that all properties of this instance have changed.
     */
    public synchronized void notifyChange() {
        if (mCallbacks != null) {
            mCallbacks.notifyCallbacks(this, 0, null);
        }
    }

    /**
     * Notifies listeners that a specific property has changed. The getter for the property
     * that changes should be marked with {@link Bindable} to generate a field in
     * <code>BR</code> to be used as <code>fieldId</code>.
     *
     * @param fieldId The generated BR id for the Bindable field.
     */
    public void notifyPropertyChanged(int fieldId) {
        if (mCallbacks != null) {
            mCallbacks.notifyCallbacks(this, fieldId, null);
        }
    }
}

4.2. 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();

4.3. Observable 容器類

一些應用會使用更加靈活的結構來保存數據。Observable 容器類允許使用 key 來獲取這類數據。
4.3.1 當 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"/>

4.3.2 當 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"/>

4.3. 雙向綁定

雙向綁定是指當 View 屬性值修改的時候,也能夠修改綁定的 model 的數據。雙向綁定用法很簡單,在要使用雙向綁定的地方,使用 “@={}” 語法即可。

<EditText android:text="@={user.firstName}" />

注意,這里的 firstName 必須是 ObservableField <T> 類型。

雙向綁定只適用于那些某個屬性綁定監聽事件的控件,如

  • TextView/EditView/Button (android:text, TextWatcher)
  • CheckBox (android:checked, OnCheckedChangeListener)
  • DatePicker(android:year, android:month, android:day, OnDateChangedListener)
  • TimePicker(android:hour, android:minute, OnTimeChangedListener)
  • RatingBar(android:rating, OnRatingBarChangeListener)

大部分控件都能滿足雙向綁定的需求,實在不行就自定義滿足該要求的控件吧。

5. 生成綁定

生成的 binding 類會將布局中的 View 與變量綁定在一起。如前所述,binding 類的類名和包名可以自定義,binding 類也會繼承 ViewDataBinding。

5.1. 創建綁定

binding 類對象應該在 inflate 之后立馬創建,來確保 View 的層次結構不會在綁定前被干擾。綁定布局的方式有好幾種。最常見的是使用 binding 類中的靜態方法。
5.1.1 Binding 類中的 inflate 函數會 inflate View 樹并將 View 綁定到 binding 對象上,一氣呵成。inflate 有一個非常簡單的版本,只需要一個 LayoutInflater 或一個 ViewGroup:

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

5.1.2 如果布局使用不同的機制來 inflate,下面方法則可以獨立來做綁定操作:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

5.1.3 有時綁定關系是不能提前確定的,這時可以使用 DataBindingUtil :

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

它只需要一個布局文件id,會根據布局文件id自動找到 Binding 類。

5.2. 帶有 ID 的 View

布局中每一個帶有 ID 的 View,在 Binding 類中都會生成一個 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 中并不是必需的,但是在某些情況下還是有必要在 java 代碼中對 View 進行操作,這時就需要 id,在 java 代碼中可以通過 binding 對象直接操作 view 對象。

5.3. 變量

每一個變量會有相應的存取函數:

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

5.4. ViewStub

ViewStub 相比普通 View 有一些不同。ViewStub 一開始是不可見的,當它們被設置為可見,或者調用 inflate 方法時,ViewStub 會被替換成另外一個布局。

因為 ViewStub 實際上不存在于 View 結構中,binding 類中的 View 對象也得移除掉,以便系統回收。因為 binding 類中的 View 都是 final 的,所以我們使用一個叫 ViewStubProxy 的類來代替 ViewStub。開發者可以使用它來操作 ViewStub,獲取 ViewStub inflate 時得到的視圖。

但 inflate 一個新的布局時,必須為新的布局創建一個 binding。因此,ViewStubProxy 必須監聽 ViewStub 的 ViewStub.OnInflateListener,并及時建立 binding。由于 ViewStub 只能有一個 OnInflateListener,你可以將你自己的 listener 設置在 ViewStubProxy 上,在 binding 建立之后, listener 就會被觸發。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:orientation="vertical"
        tools:context="com.connorlin.databinding.context.ViewStubActivity">

        <Button
            android:text="@string/inflate_viewstub"
            android:onClick="inflate"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <ViewStub
            android:id="@+id/view_stub"
            android:layout="@layout/include"
            android:layout_width="match_parent"
            android:layout_weight="1"
            android:layout_gravity="center"
            android:layout_height="wrap_content" />

    </LinearLayout>
</layout>

像下面這樣在 OnInflateListener 中建立綁定:

public class ViewStubActivity extends BaseActivity {

    private ActivityViewStubBinding mActivityViewStubBinding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mActivityViewStubBinding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
        mActivityViewStubBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub stub, View inflated) {
                // 填充完成開始創建 binding
                IncludeBinding viewStubBinding = DataBindingUtil.bind(inflated);
                User user = new User("Connor", "Lin", 28);
                viewStubBinding.setUser(user);
            }
        });
    }

    public void inflate(View view) {
        if (!mActivityViewStubBinding.viewStub.isInflated()) {
            // 觸發 viewstub 填充
            mActivityViewStubBinding.viewStub.getViewStub().inflate();
        }
    }
}

當點擊 Button 的時候會上面的調用 inflate 方法,此時觸發 viewstub 開始填充 view,并且給 viewstub 設置了監聽器,當 view 填充完成,開始創建 binding。

5.5. 高級綁定

5.5.1. 動態綁定變量

有時候,我們不知道 binding 類要綁定哪一個變量。例如,RecyclerView.Adapter 可以用來處理不同布局的時候,它就不知道應該使用 binding 類綁定那一個變量。而是在 onBindViewHolder(VH, int)) 的方法中,binding 類重新被賦值,來更新 item view 的數據。

在下面例子中,RecyclerView 中的所有布局都內置了一個 item 變量。BindingHolder 有一個 getBinding 方法,返回一個 ViewDataBinding 基類。

@Override
public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    RecyclerItemBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.recycler_item, parent, false);

    Presenter presenter = new Presenter();
    binding.setPresenter(presenter);

    BindingHolder holder = new BindingHolder(binding.getRoot());
    holder.setBinding(binding);
    return holder;
}

@Override
public void onBindViewHolder(BindingHolder holder, int position) {
    // 動態綁定變量
    holder.getBinding().setVariable(BR.item, mRecyclerItemList.get(position));
    holder.getBinding().executePendingBindings();
}

@Override
public int getItemCount() {
    return mRecyclerItemList.size();
}


public class BindingHolder extends RecyclerView.ViewHolder {
    private RecyclerItemBinding binding;

    public BindingHolder(View itemView) {
        super(itemView);
    }

    public RecyclerItemBinding getBinding() {
        return binding;
    }

    public void setBinding(RecyclerItemBinding binding) {
        this.binding = binding;
    }
}

5.5.2. 立即 binding

當變量或者 observable 發生變動時,會在下一幀觸發 binding。有時候 binding 需要馬上執行,這時候可以使用 executePendingBindings())。

5.5.3. 后臺線程問題

只要數據不是容器類,你可以直接在后臺線程做數據變動。DataBinding 會將變量/字段轉為局部量,避免同步問題。

6. 屬性 Setter

當綁定數據發生變動時,生成的 binding 類必須根據 binding 表達式調用 View 的 setter 函數來修改 View 的屬性。Data binding 框架內置了幾種自定義賦值給 view 的方法。

6.1. 自動 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}"/>

6.2. 重命名 Setter

一些屬性的命名與 setter 不對應。針對這些函數,可以用 BindingMethods 注解來將屬性與 setter 綁定在一起。例如,android:tint 屬性可以像下面這樣與 setImageTintList(ColorStateList)) 綁定,而不是 setTint,這個注解標注在任何類上面都有效果:

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

Android 框架中的 setter 重命名已經在庫中實現了,開發者只需要專注于自己的 setter。比如:

@BindingMethods({
        @BindingMethod(type = ZoomControls.class, attribute = "android:onZoomIn", method = "setOnZoomInClickListener"),
        @BindingMethod(type = ZoomControls.class, attribute = "android:onZoomOut", method = "setOnZoomOutClickListener"),
})
public class ZoomControlsBindingAdapter {
}

6.3. 自定義 Setter

6.3.1 一些屬性需要自定義 setter 邏輯。例如,目前沒有與 android:paddingLeft 對應的 setter,只有一個 setPadding(left, top, right, bottom) 函數。使用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());
}

BindingAdapter 在其他自定義類型上也很好用。比如,一個 loader 可以在非主線程加載圖片。

當存在沖突時,開發者創建的 binding adapter 方法會覆蓋 data binding 的默認 adapter 方法。

6.3.2 你也可以創建有多個參數的 Adapter 方法:

@BindingAdapter({"app:imageUrl", "app:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView 
    app:imageUrl=“@{venue.imageUrl}”
    app:error=“@{@drawable/venueError}”/>

當 imageUrl 與 error 屬性都存在時,并且 imageUrl 是一個 String,error 是一個 Drawable,這個 adapter 會被調用。

  • 屬性與自定義 setter 在匹配時,自定義命名空間會被忽略
  • 也可以為 android 命名空間編寫 adapter

6.3.3 BindingAdapter 注解標記的 setter 方法可以獲取屬性舊的賦值。要使用舊值,只需要將舊值放置在前,新值放置在后:

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

6.3.4 事件處理程序只能與一個抽象方法的接口或抽象類一起使用。例如:

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

6.3.5 當 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 或更新的設備上使用。

通過自定義 setter 可以通過在 xml 中的表達式喚起任意 view 中的方法,并且在調用 view 方法之前添加我們自己的 view 控制邏輯。

比如用于給 TextView 添加 xml 字體設置屬性:

<TextView app:font="@{`Source-Sans-Pro-Regular.ttf`}"/> 
public class AppAdapters {
  @BindingAdapter({"font"})
  public static void setFont(TextView textView, String fontName){
    AssetManager assetManager = textView.getContext().getAssets();
    String path = "fonts/" + fontName;
    Typeface typeface = sCache.get(path);
    if (typeface == null) {
      typeface = Typeface.createFromAsset(assetManager, path);
      sCache.put(path, typeface);
    }
    textView.setTypeface(typeface);
  }
}

7. 轉換器

7.1. 對象轉換

當 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) 所需要的類型。當對參數類型存在疑惑時,開發者需要手動做類型轉換。

7.2. 自定義轉換

有時候會自動在特定類型直接做類型轉換。例如,當設置背景的時候:

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

在這里,背景需要的是 Drawable,但是 color 是一個整數。當需要 Drawable 卻返回了一個整數時,int 會自動轉換成 ColorDrawable。這個轉換是在一個 BindingConversation 注解的靜態函數中實現:

@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"/>

8. Android Studio 對 Data binding 的支持

Android Studio 支持許多用于數據綁定代碼的代碼編輯功能。例如,它支持數據綁定表達式的以下功能:

  • 語法高亮顯示
  • 標記表達式語言語法錯誤
  • XML代碼完成
  • 引用,包括導航(如導航到聲明)和快速文檔

注意:數組和泛型類型(如Observable類)可能在沒有錯誤時顯示錯誤。

在“預覽”窗格可以顯示數據綁定表達式的默認值。下面是一個設置默認值的例子,TextView 的 text 默認值為 PLACEHOLDER。

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

如果需要在項目的設計階段顯示默認值,還可以使用tools屬性代替表達式默認值,詳見設計階段布局屬性

參考資料:

推薦拓展閱讀:

Demo:

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

推薦閱讀更多精彩內容