框架組件(二)Data Binding 2-布局與綁定表達式

該系列文章是對Android推出的架構組件相關文章,按作者自己理解來翻譯的,同時標記有作者自己一些簡單筆記。如果讀者發現文中有翻譯不準確的地方,或者理解錯誤的地方,請不吝指教。

源自Google官方
Data Binding Library
一文的翻譯與歸納

其他相關鏈接:
Android Jetpack Components

[TOC]

表達式語言允許你使用表達式處理View調度的方法。Data Binding 庫自動生成將布局中view與數據對象綁定的類。

Data Binding 布局文件與普通布局略有不同,根布局使用 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>

data 中的 user variable 描述了該布局可以使用的一個屬性

<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: 布局表達式需要保持短小簡介,因為他們不能進行單元測試,且IDE支持功能也有限。你可以使用自定義 binding adapter 來簡化布局表達式。

數據對象

假設我們現在有一個傳統的 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;
  }
}

從數據綁定的角度來看,上面兩個類是等價的。@{user.firstName}表達式會使用前者的 firstName 屬性和后者的 getFirstName() 方法給 android:text 屬性賦值。另外,如果存在 firstName() 方法,它也可以正常解析。

綁定數據

每個布局文件都會生成一個綁定類。默認情況下,生成的類基于布局名稱使用駝峰命名并添加Binding后綴來命名。之前的布局文件名稱是 activity_main.xml,所以對應生成類是ActivityMainBinding。該類包含所有從布局data屬性到布局view的所有綁定,并知道如何為綁定表達式賦值。推薦在布局引入創建綁定,以下是實例:

@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時,將會在界面中顯示 Test 用戶。另外,你可以使用 LayoutInflater 獲取視圖,如下所示:

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

如果在 FragmentListView 或者 RecyclerView 適配器中使用數據綁定,你可能更喜歡使用綁定類或者 DataBindingUtlinflate() 方法,如下所示:

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

表達式語言

常見特征

表達式語言看起來很像是代碼中的表達式。你可以在表達式中使用以下運算符和關鍵字:

  • 數學運算符 + - / * %
  • 字符串拼接 +
  • 邏輯運算符 && ||
  • 二進制 & | ^
  • 一元 + - ! ~
  • 位移 >> >>> <<
  • 比較 == > < >= <=
  • instanceof
  • 括號 ()
  • 字符類型 - 字符、字符串、數字、null
  • 強制轉換
  • 方法調用
  • 變量訪問
  • 數組訪問 []
  • 三目運算符 ? :

例子:

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

缺少的操作符

以下是可以再代碼中使用但是表達式不支持的操作符:

  • this
  • super
  • new
  • 顯示通用調用

Null合并 操作符

?? 操作符會在左側不為空時選擇前者,左側為空時選擇右側。

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

該操作等價于:

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

屬性引用

一個表達式可以使用以下格式引用類屬性,對于 fieldsgettersObservableField 都是一樣的格式:

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

避免空指針異常

生成的數據綁定代碼會自動檢測 null 值,避免了空指針異常。舉個例子,在表達式 @{user.name} 中,如果 user 為空,user.name 將默認分配為 null 值。如果你引用的是 user.age,其中age是 int 類型,那么數據綁定時將默認使用0。

集合

常見集合,例如 array、list、SparseArray、與 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]}"

注意: 你也可以使用 . 符號來獲取 map 中的元素。舉個例子,在上面例子中的 @{map[key]} 可以替換為 @{map.key}

字符串文本

你可以使用單引號將屬性值括起來,這將允許你在表達式中使用雙引號來表示字符串:

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

當然也可以用雙引號將屬性值括起來。這時字符串文本使用單引號來表示:

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

資源

你可以用以下表達式語法來分配resource:

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

一些資源需要指定明確的類型,如下所示:

類型 正常引用 表達式引用
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

事件處理

Data binding 允許你使用表達式來處理 view 分發的時間(例如 onClick 方法)。時間屬性名稱由 Listener 方法來決定,但有一些例外。比如 View.OnClickListener 有一個 onClick() 方法,所以該事件對應屬性值是 android:onClick

這里有一些特殊的點擊事件處理需要用 android:onClick 之外以屬性避免沖突。如下所示:

設置監聽 屬性
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

你可以用以下機制處理事件:

  • 方法引用:在你的表達式中,你可以引用符合監聽方法規定的方法。當表達式判定是一個方法時,Data binding 會將方法和方法所有者對象包裝到一個 listener 中,同時將 listener 設置到指定 view 里。如果表達式判定為 null,Data binding 會給 view 設置監聽為 null
  • listener綁定:該方法是在事件發生時計算 lambada 表達式。 Data binding 總是會創建 listener 并設置給view,當事件分發時,創建的監聽器計算 lambada 表達式。

方法引用

事件可以直接綁定到方法上,同樣的,android:onClick 可以關聯 activity的一個方法。和 View 的 onClick 屬性相比,一個主要的優點是表達式是在編譯時處理,所以如果方法不存在或者格式不對,會直接提示編譯錯誤。

方法引用和listener綁定主要不同點是,當數據綁定時才創建實際的 listener 實例,而不是觸發事件時。如果你希望事件觸發時計算表達式,你應該使用 listener 綁定。

要將事件分配給處理者,需要使用不同綁定表達式,即值設置為要調用的方法名稱。如下示例:

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

如下設置,綁定表達式可以將 view 單擊監聽分配給 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>

注意:表達式內方法必須與 listener 重寫的方法格式、參數完全一致。

Listener 綁定

Listener 綁定是在事件發生時運行綁定表達式。和方法引用十分類似,但是它們可以讓你運行任意數據綁定表達式。這個功能只適用于Gradle版本2.0及更高版本。

在方法引用中,指定方法的參數必須與事件 listener 方法參數匹配。在 listener 綁定中,只需要你的返回值與 listener 返回值匹配(或者返回值是void)。舉個例子,思考下面 有 onSaveClick 方法的 presenter 類。

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

然后你可以像下面這樣綁定 click 事件到 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 自動為事件創建必要的 listener。當 view 觸發事件時,data binding 計算對應的表達式。與常規表達式一樣,在計算這些監聽表達式時,代碼依然會判空且是線程安全的。

在前面的例子中,我們還沒有定義過給 onClick 傳遞 view 參數。對于監聽參數 Listener 綁定有兩種選擇:你可以忽略方法的所有參數或聲明所有參數。如果你更喜歡聲明參數,你可以在表達式中這樣使用:

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

或者你希望使用表達式中的view參數,可以這樣做:

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

你可以使用有多個參數的 lambada 表達式:

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 ,你的表達式也必須返回相同類型的值。比如,如果你監聽的是 long click 事件,你的表達式必須返回一個 boolean 值。

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

如果由于 null 對象導致表達式無法計算,data binding 會返還對應類型的默認值。比如對象引用對應 nullint 對應 0boolean 值對應 false 等等。

如果你的表達式有?:??之類的計算,你可以使用 void 來作為一個元素。如下所示:

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

避免復雜監聽

監聽表達式很強大,且可以讓你的代碼更簡單易懂。但另一方面,如果監聽表達式復雜會讓你的 layout 更難理解和維護。所以這些表單式應該僅僅用于將數據從 UI 傳遞到你的回調方法。你應該在回調方法中來實現業務邏輯,而不應該在監聽表達式中實現。

import,variable 和 include

Data Binding 庫提供了 importvariableinclude 這些功能。import 讓你的布局文件更輕松的引用類;variable 允許你定義能在綁定表達式中使用的屬性;include 讓你可以重用復雜的布局。

imports

Import 允許你在布局文件中輕松引用類,就像在代碼里一樣。data 元素中可以定義0個或多個 import 元素。如下示例展示在布局文件中引入 View 類:

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

引入 View 類后允許你在綁定表達式中使用它。下面例子展示了如何使用 View 類中的 VISIBLEGONE 常量。

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

類型別名

當多各類名稱沖突時,其中某些類可以定義一個別名。下面例子展示了將在 com.example.real.estate 包中的 View 類重命名為 Vista

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

這樣在布局文件中就可以用 Vista 表示 com.example.real.estate.View 類,而 View 表示 android.view.View

import 其它類

引入類型可以用作變量或者表達式中的類型引用。下面例子展示了 UserList 用作變量的類型:

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List<User>"/>
</data>

你也可以在表達式中使用引入類型強制轉換。就像下面的例子一樣:

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

這些 variable 類型會在編譯時被檢測,因此如果 variable 實現了 Observable 或者 observable collection,必須在類型中體現。如果 variable 是沒有實現 Observable 接口的基類或接口,將不能被觀測。

當存在不同配置下的布局文件時(比如橫屏、豎屏),variable 將被組合。這些布局文件之間不能存在沖突的變量定義。

生成的 binding 類中每個 variable 都有 getset 方法。在給這些 variable 設置值之前,都會使用對應類型默認值。比如引用類型為null,int 為0,booleanfalse 等等。

一個特殊的 variable 名稱是 context ,為需要的表達式生成,該變量是由根 view 的 getContext() 方法獲取的 Context 對象。如果要覆蓋 context 變量,則需要顯示聲明 variable 來覆蓋。

Includes

variable 可以用通過 includ 標簽的 bind 屬性傳遞到引入的布局中。下面例子展示了 name.xmlcontact.xml 包含 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>

Data binding 不支持 include 直接作為 merge 元素的直接子元素,下面示例是錯誤示范:

<?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>
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容

  • 出個園南門我們已經走在了揚州東關一條街上,街上人來人往,狹窄的小巷被人群擠的滿滿的,各色小店林立其中,還有叫...
    明亮自然閱讀 1,585評論 0 3
  • 在假期即將結束,工作日就快到來的時刻,我們有時會感到焦慮,覺得假期還沒有被充分利用,想做的事情還有很多。這...
    鮑弘軍閱讀 186評論 0 0
  • 寂寞是一個人的狂歡 狂歡是一群人的寂寞 想你并不是因為寂寞 寂寞卻又是因為想你 最好的選擇就是忘記 忘記最終選擇了...
    瀚正閱讀 139評論 0 4
  • 你在家~ 家里感覺像過年似的~ 我和老爸每天早早地出去采買食材~ 只為給你做可口的飯菜~ 老爸幫廚的過程中樂呵呵的...
    梅子吉祥如意懷德閱讀 181評論 0 2
  • 受眾分析《博韋商務溝通》,270頁 作者是(美)考特蘭·博韋 【R閱讀原文片段】 為了拿出有說服力的分析性報告,在...
    vicky思捷閱讀 613評論 4 49