Android Jetpack架構(gòu)篇:Data Binding(一)
<spane id="layouts_binding_exp">Layouts and binding expressions(布局和綁定表達(dá)式)</span>
該表達(dá)式語言,允許你通過編寫表達(dá)式來處理視圖的事件分發(fā)。數(shù)據(jù)綁定庫(Data Binding Library)自動生成將布局中的視圖與data對象綁定所需的類。
數(shù)據(jù)綁定布局文件略有不同,必須以 layout 標(biāo)記開頭,后跟 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文本設(shè)置為user變量的 firstName屬性:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
注意:布局表達(dá)式應(yīng)保持短小且簡單,因為它們不能進(jìn)行單元測試并且IDE支持有限。為了簡化布局表達(dá)式,您可以使用自定義綁定適配器。
Data object(數(shù)據(jù)對象)
假設(shè)有一個普通的實體對象User:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
這種類型的對象具有永不改變的數(shù)據(jù)。在應(yīng)用程序中,通常會讀取一次并且之后不會更改的數(shù)據(jù)。也可以使用具有約定規(guī)則的寫法,例如訪問器方法的用法,如以下示例所示:
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;
}
}
從數(shù)據(jù)綁定的角度來看,這兩個類是等價的。@{user.firstName}用于該android:text 屬性的表達(dá)式訪問firstName前一類中的字段和getFirstName()后一類中的 方法?;蛘撸?strong>firstName()如果存在該方法,也可以解決。
Binding data(綁定數(shù)據(jù))
為每個布局文件生成綁定類。默認(rèn)情況下,類的名稱基于布局文件的名稱,將其轉(zhuǎn)換為Pascal大小寫并向其添加Binding后綴。上面的布局文件名是 activity_main.xml相應(yīng)生成的類 MainActivityBinding。此類包含布局屬性(例如,user變量)到布局視圖的所有綁定,并知道如何為綁定表達(dá)式指定值。建議綁定的推薦方法是在擴(kuò)展布局時執(zhí)行此操作,如圖所示在以下示例中:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
注意:上面MainActivityBinding是官網(wǎng)中寫法,正確寫法是ActivityMainBinding請讀者自行替換,下同。
在運(yùn)行時,應(yīng)用程序在UI中顯示Test用戶?;蛘?,您可以使用a獲取視圖LayoutInflater,如以下示例所示:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果您在 Fragment, ListView或RecyclerView適配器中使用數(shù)據(jù)綁定,則可能更適合使用 inflate() 綁定類或 DataBindingUtil類的方法,如以下代碼示例所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
表達(dá)語言
一般特征
表達(dá)式語言看起來很像托管代碼中的表達(dá)式。您可以在表達(dá)式語言中使用以下運(yùn)算符和關(guān)鍵字:
- 基本運(yùn)算 + - / * %
- 字符串連接 +
- 邏輯表達(dá)式 && ||
- 二進(jìn)制 & | ^
- 一元運(yùn)算符 + - ! ~
- 移位 >> >>> <<
- 比較運(yùn)算 == > < >= <=
- instanceof
- 分組 ()
- 字面值 - 字符,字符串,數(shù)字, null
- 強(qiáng)轉(zhuǎn)
- 方法調(diào)用
- 屬性訪問
- 數(shù)組訪問 []
- 三元運(yùn)算符 ?:
舉例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
缺少操作
您可以在托管代碼中使用的表達(dá)式語法中缺少以下操作:
- this
- super
- new
- 顯式通用調(diào)用
空結(jié)合運(yùn)算符
空合并運(yùn)算符(??)如果不是,則選擇左操作數(shù),如果是null 前者,則選擇右操作數(shù)null。
android:text="@{user.displayName ?? user.lastName}"
這在功能上等價于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
屬性引用
表達(dá)式可以使用以下格式引用類中的屬性,這對于字段,getter和ObservableField 對象是相同的 :
android:text="@{user.lastName}"
空指針異常避免
生成的數(shù)據(jù)綁定代碼會自動檢查null值并避免空指針異常。例如,在表達(dá)式中@{user.name},如果 user為null,user.name則為其分配其默認(rèn)值null。如果您引用user.ageage,其中age是類型int,則數(shù)據(jù)綁定使用默認(rèn)值0。
集合
可以使用[]運(yùn)算符訪問普通集合,例如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]}"
注意:您還可以使用object.key表示法引用map中的值。例如,@{map[key]}在上面的示例中可以替換為 @{map.key}。
字符串文字
您可以使用單引號括起屬性值,這允許在表達(dá)式中使用雙引號,如以下示例所示:
android:text='@{map["firstName"]}'
也可以使用雙引號來包圍屬性值。這時,字符串文字應(yīng)該用后引號括起來`:
android:text="@{map[`firstName`]}"
資源
可以使用以下語法訪問表達(dá)式中的資源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
可以通過提供參數(shù)來評估格式字符串和復(fù)數(shù):
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
當(dāng)復(fù)數(shù)采用多個參數(shù)時,應(yīng)傳遞所有參數(shù):
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
某些資源需要顯式類型求值,如下表所示:
| 類型 | 正常引用 | 表達(dá)式引用 |
|---|---|---|
| String[] | @array | @stringArray |
| int[] | @array | @intArray |
| TypedArray | @array | @typedArray |
| Animator | @animator | @animator |
| StateListAnimator | @animator | @stateListAnimator |
| color int | @color | @color |
| ColorStateList | @color | @colorStateList |
事件處理
數(shù)據(jù)綁定允許您編寫從視圖調(diào)度的表達(dá)式處理事件(例如,onClick()方法)。事件屬性名稱由監(jiān)聽器方法的名稱確定,但有一些例外。例如,View.OnClickListener有一個方法onClick(),所以這個事件的屬性是android:onClick。
對于click事件,有一些專門的事件處理程序需要除android:onClick避免沖突之外的屬性。您可以使用以下屬性來避免這些類型的沖突:
| Class | Listener setter | Attribute |
|---|---|---|
| SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
| ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
| ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
您可以使用以下機(jī)制來處理事件:
- 方法引用:在表達(dá)式中,您可以引用符合監(jiān)聽器方法簽名的方法。當(dāng)表達(dá)式求值為方法引用時,Data綁定將方法引用和所有者對象包裝在監(jiān)聽器中,并在目標(biāo)view上設(shè)置該監(jiān)聽器。如果表達(dá)式求值為null,則數(shù)據(jù)綁定不會創(chuàng)建監(jiān)聽器并設(shè)置null監(jiān)聽器。
- 監(jiān)聽器綁定:這些是在事件發(fā)生時計算的lambda表達(dá)式。數(shù)據(jù)綁定總是創(chuàng)建一個監(jiān)聽器,它在view上設(shè)置。調(diào)度事件時,監(jiān)聽器會計算lambda表達(dá)式。
方法引用
事件可以直接綁定到處理程序方法,類似于android:onClick 可以分配給活動中的方法的方式。與View onClick屬性相比的一個主要優(yōu)點(diǎn) 是表達(dá)式在編譯時處理,因此如果該方法不存在或其簽名不正確,則會收到編譯時錯誤。
方法引用和監(jiān)聽器綁定之間的主要區(qū)別在于,實際的監(jiān)聽器實現(xiàn)是在綁定數(shù)據(jù)時創(chuàng)建的,而不是在觸發(fā)事件時創(chuàng)建的。如果您希望在事件發(fā)生時評估表達(dá)式,則應(yīng)使用監(jiān)聽器綁定。
要將事件分配給其處理程序,請使用普通綁定表達(dá)式,其值為要調(diào)用的方法名稱。例如,請考慮以下示例數(shù)據(jù)對象:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
綁定表達(dá)式可以將View的單擊監(jiān)聽器分配給 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>
注意:表達(dá)式中方法的簽名必須與監(jiān)聽器對象中方法的簽名完全匹配。
監(jiān)聽器綁定
監(jiān)聽器綁定是在事件發(fā)生時運(yùn)行的綁定表達(dá)式。它們類似于方法引用,但它們允許您運(yùn)行任意數(shù)據(jù)綁定表達(dá)式。適用于Gradle版本2.0及更高版本的Android Gradle插件提供此功能。
在方法引用中,方法的參數(shù)必須與事件監(jiān)聽器的參數(shù)匹配。在監(jiān)聽器綁定中,只有您的返回值必須與監(jiān)聽器的預(yù)期返回值匹配(除非它期望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>
在表達(dá)式中使用回調(diào)時,數(shù)據(jù)綁定會自動創(chuàng)建必要的監(jiān)聽器并注冊事件。當(dāng)View觸發(fā)事件時,數(shù)據(jù)綁定會計算給定的表達(dá)式。與常規(guī)綁定表達(dá)式一樣,在計算這些監(jiān)聽器表達(dá)式時,仍然可以獲得數(shù)據(jù)綁定的null和線程安全性。
在上面的示例中,我們尚未定義view傳遞給的參數(shù)onClick(View)。監(jiān)聽器綁定為監(jiān)聽器參數(shù)提供了兩種選擇:忽略方法的所有參數(shù),也可以命名所有參數(shù)。如果為參數(shù)命名,則可在表達(dá)式中使用它們。例如,上面的表達(dá)式可以寫成如下:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
或者,如果要在表達(dá)式中使用該參數(shù),則可以按如下方式工作:
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
使用帶有多個參數(shù)的lambda表達(dá)式:
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)}" />
如果監(jiān)聽事件返回的類型不是void,則你的表達(dá)式也必須返回相同類型的值。例如,如果要監(jiān)聽長按事件,則應(yīng)返回表達(dá)式boolean。
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于null對象而無法計算表達(dá)式,則數(shù)據(jù)綁定將返回該類型的默認(rèn)值。例如:引用類型為null對于參考, int為0,boolean為false等。
如果需要使用帶斷言的表達(dá)式(例如:三元運(yùn)算),則可以使用void。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免復(fù)雜的監(jiān)聽器
監(jiān)聽器表達(dá)式非常強(qiáng)大,可以使您的代碼非常容易閱讀。另一方面,包含復(fù)雜表達(dá)式的監(jiān)聽器使您的布局難以閱讀和維護(hù)。這些表達(dá)式應(yīng)該像將UI中的可用數(shù)據(jù)傳遞給回調(diào)方法一樣簡單。您應(yīng)該在從監(jiān)聽器表達(dá)式調(diào)用的回調(diào)方法中實現(xiàn)任何業(yè)務(wù)邏輯。
Imports、variables和includes
數(shù)據(jù)綁定庫提供導(dǎo)入,變量和包含等功能。導(dǎo)入使布局文件中的類很容易引用。變量允許您描述可用于綁定表達(dá)式的屬性。包括讓您在整個應(yīng)用中重復(fù)使用復(fù)雜的布局。
Imports
導(dǎo)入允許您輕松引用布局文件中的類,就像在托管代碼中一樣。import可以在data 元素內(nèi)使用零個或多個元素。以下代碼示例將View類導(dǎo)入布局文件:
<data>
<import type="android.view.View"/>
</data>
導(dǎo)入的View類,可以在綁定表達(dá)式中引用它。以下示例顯示如何引用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(類型別名)
當(dāng)存在類名沖突時,可以將其中一個類重命名為別名。以下示例將包中的View類 重命名com.example.real.estate為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。
導(dǎo)入其他類
導(dǎo)入的類型可以用作變量和表達(dá)式中的類型引用。以下示例顯示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>
警告:Android Studio尚未處理導(dǎo)入,因此導(dǎo)入變量的自動完成功能可能無法在IDE中運(yùn)行。您的應(yīng)用程序仍在編譯,您可以通過在變量定義中使用完全限定名稱來解決IDE問題。
您還可以使用導(dǎo)入的類型來進(jìn)行強(qiáng)轉(zhuǎn)。以下示例將connection屬性強(qiáng)制類型轉(zhuǎn)換為User:
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在表達(dá)式中引用靜態(tài)字段和方法時,也可以使用導(dǎo)入的類型。以下代碼導(dǎo)入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.*會自動導(dǎo)入。
variables(變量)
您可以variable在元素內(nèi)使用多個元素data。每個 variable元素都描述了一個屬性,該屬性可以在布局中設(shè)置,以便在布局文件中的綁定表達(dá)式中使用。下面的示例聲明的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>
在編譯時檢查變量類型,因此如果變量實現(xiàn)Observable或observable collection,則應(yīng)該在類型中反射。如果變量的基類或接口未實現(xiàn)Observable接口,則不會觀察變量。
當(dāng)存在用于各種配置的不同布局文件(例如,橫向或縱向)時,組合變量。這些布局文件之間不得存在沖突的變量定義。
生成的綁定類對于每個描述的變量都有一個setter和getter。調(diào)用setter之前,變量采用托管代碼默認(rèn)值 -- 引用類型為null,int為0,boolean為false等。
特殊變量context,可由綁定表達(dá)式生成。context的值是由根View的getContext()方法獲取的Context對象。context變量由具有該名稱的顯式變量聲明覆蓋。
includes(包含)
通過使用app命名空間和屬性中的變量名,變量可以從包含的布局傳遞到包含的布局綁定中。以下示例顯示user了name.xml和 contact.xml布局文件中包含的變量:
<?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>
數(shù)據(jù)綁定不支持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>
參考
[1] https://developer.android.com/topic/libraries/data-binding/
[2] https://developer.android.com/topic/libraries/data-binding/expressions