該系列文章是對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());
如果在 Fragment
,ListView
或者 RecyclerView
適配器中使用數據綁定,你可能更喜歡使用綁定類或者 DataBindingUtl
的 inflate()
方法,如下所示:
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}"
屬性引用
一個表達式可以使用以下格式引用類屬性,對于 fields
、getters
和 ObservableField
都是一樣的格式:
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 會返還對應類型的默認值。比如對象引用對應 null
,int
對應 0
, boolean
值對應 false
等等。
如果你的表達式有?:
或??
之類的計算,你可以使用 void
來作為一個元素。如下所示:
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免復雜監聽
監聽表達式很強大,且可以讓你的代碼更簡單易懂。但另一方面,如果監聽表達式復雜會讓你的 layout 更難理解和維護。所以這些表單式應該僅僅用于將數據從 UI 傳遞到你的回調方法。你應該在回調方法中來實現業務邏輯,而不應該在監聽表達式中實現。
import,variable 和 include
Data Binding 庫提供了 import
、variable
和 include
這些功能。import
讓你的布局文件更輕松的引用類;variable
允許你定義能在綁定表達式中使用的屬性;include
讓你可以重用復雜的布局。
imports
Import 允許你在布局文件中輕松引用類,就像在代碼里一樣。data
元素中可以定義0個或多個 import
元素。如下示例展示在布局文件中引入 View
類:
<data>
<import type="android.view.View"/>
</data>
引入 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}"/>
類型別名
當多各類名稱沖突時,其中某些類可以定義一個別名。下面例子展示了將在 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 其它類
引入類型可以用作變量或者表達式中的類型引用。下面例子展示了 User
和 List
用作變量的類型:
<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 都有 get
和 set
方法。在給這些 variable 設置值之前,都會使用對應類型默認值。比如引用類型為null,int
為0,boolean
為 false
等等。
一個特殊的 variable 名稱是 context
,為需要的表達式生成,該變量是由根 view 的 getContext()
方法獲取的 Context
對象。如果要覆蓋 context
變量,則需要顯示聲明 variable 來覆蓋。
Includes
variable 可以用通過 includ 標簽的 bind
屬性傳遞到引入的布局中。下面例子展示了 name.xml
和 contact.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>