1.簡介

表達式語言(expression language)允許開發者編寫處理視圖調度事件的表達式。
Data Binding Library 會自動生成視圖與數據對象綁定所需的類別(綁定類別)(binding class)。
數據綁定佈局文件(data binding layout files)和一般佈局文件不同。基本上會以 layout 標籤開頭,後面跟著 data 元素和 view 的元件。這個 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 元素的 user 變數代表可在這個佈局中使用的屬性。

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

表達式透過 @{} 語法將變數和佈局結合,如下 TextView 將被設定給 user 變數的 firstname 屬性。

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

Note:
因為佈局表達式(layout expression)無法進行單元測試,因此應該保持小而簡單,開發者可以透過 binding adapter 保持簡潔。

2.Data Object(資料物件)

假設有個 User 類別如下

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

這種類型的物件具有永不改變的數據,該數據通常會讀取一次且不會更改。也可以使用遵循一組約定的對象,例如 Java 中的訪問器方法,如以下

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

從數據綁定的角度來看,這兩個類別是相同的。
以下方表達式而言

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

等同於存取第一個類別的 public final String firstName 變數,也等同於呼叫第二個類別的 getFirstName 方法
以編程方式來呈現如下

TextView firstName = (TextView)findViewById(….)
firstName.setText(user.firstName);
or
firstName.setText(user.getFirstName());

3.Binding Data(綁定類別)

Data Binding 基本上會為每個佈局文件產生綁定類別(binding class)。
預設綁定類別的名稱會根據佈局文件的名稱建立,建立方式為將其轉換 Pascal 大小寫並添加 Binding 後綴。
舉例來說若佈局文件名稱為 activity_main.xml,則相對應的綁定類別為 ActivityMainBinding。
綁定類別將會持有對應佈局文件的所有綁定內容(如 user 變數)。並知道如何為綁定表達式指定數值。
建立綁定類別的推薦方式為在擴展佈局時建立。如下

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   
   ActivityMainBinding binding = DataBindingUtil.setContentView(this,    R.layout.activity_main);
   User user = new User("Test", "User");
   binding.setUser(user);
}

在執行 App 時,將在 UI 中顯示 Test。或可以使用 LayoutInflater 獲取視圖,如下:

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

若開發者在 Fragment,ListView 或 RecyclerView adapter 內使用數據綁定,則可以使用綁定類別或 DataBindingUtil 類別的 inflate 方法,如下:

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

4.Expression Language(表達式語言)

一般特性

數學 字串連接 邏輯 二進制 一元
+ – / * % + && || & | ^ + – ! ~
移位 比較 測試型態 分組 文字
>> >>> << == > < >= <= Instanceof () character, String, numeric, null
轉型 方法呼叫 欄位存取 陣列存取 三元運算符
Cast Method calls Field access [] ? :

範例

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

目前不支援的運算符

This Super New Explicit generic invocation
(顯式通用調用)

空結合運算符(Null coalescing operator) ??

?? 表示為空結合運算符,若該運算符的左邊為 null 則選擇運算符的左邊,若該運算符的左邊不為 null 則選擇運算符的右邊。如下

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

等同於

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

屬性參考(Porperty Reference)

表達式可以參考類別的屬性,也可以使用於 fields, getters, ObservableField objects

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

避免空指標異常(avoiding null pointer exception)
綁定類別會自動檢查空指標異常,並提供預設值。
若表達式 @{user.name} 若 user 為 null 則提供預設值 null (字串)。
若表達式 @{user.age } 且 age 的型別為 int,若 user 為 null 則提供預設值0。

集合(Collection)

一般的集合,如 array, list, 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]}"

Note:
也可以透過 object.key 來參考 map 的 元素。
如 android:text=”@{map[key]}” 等同於 android:text=”@{map.key}”

字串

可以使用單引號表示屬性值,或是使用雙引號表示字串。

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

5.Event handling(事件處理)

Data Binding 可以透過表達式處理從視圖發送的事件(如 onClick() 方法)。
事件屬性名稱由監聽器的方法名稱決定,但有一些例外。如 View.OnClickListener 有方法為 onClick(), 則該事件的屬性為 android:onClick
除了 android:onClick 以外還有一些點擊事件的特殊項目。這些特殊項目必須使用不同的屬性來避免衝突。如下

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

可以使用以下機制來處理事件
1.方法引用(method references)
在表達式中可以引用符合偵聽器方法簽名的方法。
當表達式為方法引用時,Data Binding 會包裝方法引用(method reference)和所有者物件(owner object)在監聽器中並將監聽器設定到目標視圖。
若方法引用為 null 則 Data Binding 不會建立監聽器並設定 null
2.監聽器綁定
當事件發生時使用 lambda 表示式。
Data Binding 會建立監聽器並設定到目標視圖上。當處理事件時,監聽器將會執行 lambda 表示式。

方法引用(method reference)

事件可以直接綁定到方法,類似於 android:onClick 可以指定給 Activity 的方法。
與 View onClick 相比,主要優點是表達式在編譯時期處理,因此如果該方法不存在或其簽名不正確,會收到編譯錯誤。
方法引用和偵聽器綁定的主要區別在於偵聽器實現是在綁定數據時建立的,而不是在觸發事件時建立。
若想在事件發生時再執行表達式,選擇監聽器綁定較好。
使用普通綁定表達式(normal binding expression)將事件分配給其處理者,其值為要調用的方法名稱。如下類別

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

表達式可以綁定視圖的點擊監聽器(click listener)到 onClickFriend 方法,如下

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

Note:
表達式方法的簽名必須和監聽器對象方法的簽名完全相同。

監聽器綁定(listener binding)

監聽器綁定為當事件發生時才執行綁定的表達式。類似於方法引用,但可以運行任意數據綁定表達式。該特性適用於 Android Gradle 2.0 或以上的版本。
在方法引用中,方法的參數必須和事件監聽器的參數完全相同。
監聽器綁定則是方法的回傳值和監聽器的回傳值必須相同(除非是回傳 null)。
如下類別

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

可綁定點擊事件到 onSaveClick 方法,如下

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

在表達式使用 callback 時,Data Binding 會自動建立需要的監聽器並註冊到事件。當視圖發送事件時,Data Binding 會執行相對的表達式。當表達式執行時可以確保空指針(null)和執行緒安全。
在上面的示例中,我們未定義傳遞給 onClick(View)的視圖參數。
監聽器綁定提供了 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 表達式提供超過 1 個的參數。

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

如果表達式執行結果為 null,則數據綁定將返回該類型的默認值。例如,引用類型為 null,int 為 0,布林值為 false 等。
若在表達式使用三元運算符則可使用 void,如下

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

避免複雜的監聽器

監聽器表達式可以讓代碼非常容易了解。若表達式過於複雜也會使佈局難以閱讀和維護。
這些表達式應該像 UI 的可用數據傳遞給回調方法一樣簡單並實現業務邏輯在監聽器表達式呼叫的回調方法中。

6. Imports, variables, and includes

Imports 用來在 layout 中可以參考到類別,variable 用來描述在表達式使用的屬性, includes 可在整個應用中重複使用複雜的佈局。
 

Imports

可在 layout file 簡單的參考到類別,必須寫在 data 元素裡面。如下 import View 類別到 layout file 中。

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

Import View 類別之後便可在表達式中參考它。如下在表達式中使用 View 類別的  VISIBLE 和 GONE 常數。

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

 型別別名(Type aliases)

若類別名稱發生了衝突,當中的類別便可重命名為別名。如下將 com.example.real.estate 的 View 類別重新命名為 Vista。

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

Import other classes

import 的型別可以在變數或表達式中用來當作型別參考,如下

<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 還未完全支援 import,因此自動完成的功能還無法使用在 IDE 中。
Import 的型別也可以用於轉型。如下將 connection 屬性轉型成 User

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

在表達式中也可以使用 import 型別來參考靜態變數或方法,如下 import MyStringUtils 並使用其 capitalize 方法。

<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.* 會自動 import。
 

Variables

可以在 data 元素中使用多個 variable元素,每個 variable 元素代表可能會使用在表達式的屬性。如下宣告了 user, image, note

<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 將會被反射到型別上。
若變數是基本類別或介面且未實作 Observable ,則該變數是無法被觀察(observable)
當不同佈局文件用於各種配置(橫向或縱向)時,這些變量會被組合。佈局文件之間不可存在互相衝突的變數名稱定義。
每個變數都有 setter 和 getter 方法定義在綁定類別中。變數將會使用預設數值,如整數為 0,布林為 false,參考型別則為 null。
有個特別變數為 context 會自動產生以便用於表達式,該變數的數值為根視圖(root view) getContext 方法的回傳值。

Includes

透過使用 App 的命名空間和屬性的名稱,變數可以從包含的佈局傳遞到佈局綁定中。如下從 name.xml 和 contact.xml 佈局中 include 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>

注意 : 不支援從 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><!-- Doesn't work -->
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>