前幾天在忙一些其他的東西,DataBinding 這個(gè)系列的博客本應(yīng)該在五月月初就要寫的,結(jié)果一直拖到了現(xiàn)在,罪過罪過。
在學(xué)習(xí) DataBinding 的過程中,參考 Google 官方的 DataBinding 示例 Demo,自己寫了一個(gè) DataBindingPractice Demo,用于練手。整個(gè)工程采用 MVP 架構(gòu) + DataBinding,歡迎 star、fork 和溝通交流。
本文介紹了 Data Binding 的一些基本概念和基本用法,主要包括以下四部分內(nèi)容:
- Data Binding 的介紹
- Data Binding 中的布局文件
- Data Binding 中的事件處理
- Data Binding 中的布局詳情
Data Binding 的介紹
簡(jiǎn)介
在 Data Binding 庫之前,我們經(jīng)常會(huì)寫一些重復(fù)性很高而且毫無營養(yǎng)的代碼,比如:findViewById()、setText()、setOnClickListener() 等。使用 Data Binding 庫以后,可以使用聲明式布局文件來減少粘結(jié)業(yè)務(wù)邏輯和布局文件的膠水代碼。
- Data Binding 具有良好的靈活性和兼容性,它是一個(gè) support 庫,向后兼容至 Android 2.1(API Level 7+)。
- 若使用 Android Studio 開發(fā)環(huán)境開發(fā) Android 應(yīng)用程序,則必須滿足以下兩個(gè)條件才可以使用 Data Binding 庫:
* Gradle Plugin 版本必須是 1.5.0-alpha1 或以上的版本
* Android Studio 的版本必須是1.3或以上的版本。
Data Binding 環(huán)境構(gòu)建
在 Module 的 build.gradle 中添加如下代碼,這樣應(yīng)用就支持 Data Binding 庫了。
android {
....
dataBinding {
enabled = true
}
}
注意:若 app Module 依賴了一個(gè)使用 Data Binding 的庫,則 app Module 的 build.gradle 也必須配置 Data Binding 庫。
Data Binding 中的布局文件
第一個(gè) data binding 表達(dá)式
與傳統(tǒng)的布局文件相比,data binding 布局文件與其只有輕微的不同,data binding 布局文件中的根元素是 <layout> 標(biāo)簽,其中包含一個(gè) <data> 標(biāo)簽和一個(gè) <view> 標(biāo)簽,這個(gè) <view> 標(biāo)簽的內(nèi)容與普通布局文件的內(nèi)容相同。如下所示:
<?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>
- 在 `` 標(biāo)簽中的
user變量是一個(gè)在這個(gè) data binding 布局文件中會(huì)用到的屬性。- 在 data binding 布局文件中,data binding 表達(dá)式使用
@{}語法。
數(shù)據(jù)對(duì)象(Data Object)
- 假設(shè)有一個(gè) 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;
}
}
- 還有一個(gè) User 類,是 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;
}
}
- 這兩個(gè) User 類對(duì)于 Data Binding 庫來說是等價(jià)的。在 TextView 中的
android:text屬性@{user.firstName}會(huì)使用 POJO User類中的firstName字段,或者 JavaBeans User 類中的getFirstName()方法。
綁定對(duì)象(Binding Data)
- 默認(rèn)情況下,基于 data binding 布局文件會(huì)生成一個(gè) Binding 類,此 Binding 類是將布局文件的名稱轉(zhuǎn)換成帕斯卡命名,并在之后接上
Binding命名的。比如,布局文件名稱是activity_main.xml,則其對(duì)應(yīng)的 Binding 類是ActivityMainBinding。這個(gè) Binding 類包含了布局文件中所有的布局屬性和布局視圖的綁定關(guān)系,并且知道如何向 data binding 表達(dá)式賦值。在inflate的時(shí)候,是創(chuàng)建 binding 關(guān)系最簡(jiǎn)單的時(shí)候,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
// 下面這行生成 Binding 類的代碼和上面這行生成 Binding 類的代碼是等價(jià)的。
// MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
User user = new User("Test", "User");
binding.setUser(user);
}
- 如果在 ListView 或者 RecyclerView 中的 Item 中使用 Data Binding,可以使用如下方式生成每個(gè) Item 對(duì)應(yīng)的 Binding 類,如下所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
Data Binding 中的事件處理
Data Binding 庫允許使用 data binding 表達(dá)式處理由 View 分發(fā)的事件。事件屬性的名字由 Listener 中的方法名稱決定。例如,在 View.onLongClickListener() 中有一個(gè) onLongClick() 方法,則這個(gè)事件對(duì)應(yīng)的屬性名稱是 android:onLongClick。有兩種方法處理一個(gè)事件:
- 方法引用:在表達(dá)式中,可以引用符合監(jiān)聽器方法簽名的處理方法。
- 監(jiān)聽綁定:在表達(dá)式中,是使用一個(gè) Lambda 表達(dá)式處理事件的。
兩者的區(qū)別,官方說法:方法引用和監(jiān)聽綁定的主要區(qū)別是,方法引用中監(jiān)聽器的實(shí)現(xiàn)是在數(shù)據(jù)綁定期間完成的,而不是在觸發(fā)事件時(shí)創(chuàng)建的。如果更偏向于在事件發(fā)生時(shí)再計(jì)算表達(dá)式的值,則應(yīng)該使用監(jiān)聽綁定??梢岳斫鉃椋悍椒ㄒ檬窃诰幾g期處理,而監(jiān)聽綁定是在事件分發(fā)時(shí)處理。
方法引用(Method References)
- 在方法引用中,可以直接將事件綁定到一個(gè)處理類的方法上去,類似于
android:onClick可以指定到一個(gè) Activity 中的方法。和View.onClick相比,方法引用表達(dá)式的一個(gè)主要優(yōu)點(diǎn)是:方法引用是在編譯期處理的,所以如果引用的方法不存在或者方法的簽名不匹配的話,在編譯期就會(huì)報(bào)錯(cuò)。 - 若要將一個(gè)事件指派給一個(gè)處理類,則需要使用一個(gè)正常的 data binding 表達(dá)式,這個(gè) data binding 表達(dá)式的值是將要調(diào)用的方法的名稱。例如,有一個(gè)類如下所示:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
data binding 表達(dá)式可以為 View 指定點(diǎn)擊監(jiān)聽器,如下所示:
<?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>
注意:在 data binding 表達(dá)式中的方法簽名必須和監(jiān)聽器對(duì)象中的方法簽名匹配,引用的方法的參數(shù)必須事件監(jiān)聽器的方法的參數(shù)匹配。
監(jiān)聽綁定(Listener Bindings)
監(jiān)聽綁定是在事件發(fā)生時(shí)才會(huì)運(yùn)行的 data binding 表達(dá)式。
- 監(jiān)聽綁定在 Gradle Plugin 2.0及更新的版本上才可以使用
- 和方法引用類似,不過它允許你運(yùn)行任意數(shù)據(jù)綁定表達(dá)式(不限制處理方法的參數(shù))
- 在監(jiān)聽綁定中,引用的方法的返回值和事件監(jiān)聽器期望的返回值匹配即可(除非它期望是void的)
- 例如,有一個(gè) Presenter 類如下所示:
public class Presenter {
public void onSaveClick(Task task){}
}
可以將點(diǎn)擊事件綁定到這個(gè) presenter 類上,如下所示:
<?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>
- 監(jiān)聽器由 Lambda 語句表達(dá),并且只允許作為表達(dá)式的根元素使用。
- 如果在表達(dá)式中會(huì)使用一個(gè)回調(diào),Data Binding會(huì)自動(dòng)的創(chuàng)建必要的監(jiān)聽器并將其注冊(cè)到對(duì)應(yīng)的事件。當(dāng)該控件的事件發(fā)生時(shí),Data Binding會(huì)計(jì)算表達(dá)式的值。
- 在常規(guī)的綁定表達(dá)式中,當(dāng)監(jiān)聽器的表達(dá)式計(jì)算式,Data Binding會(huì)保證綁定表達(dá)式中引用變量的空值安全性和線程安全性。
- 請(qǐng)注意,在上面的例子中,沒有定義傳遞進(jìn)
onClick()中的View參數(shù)。監(jiān)聽綁定為監(jiān)聽器的參數(shù)提供了兩種選擇:要么把參數(shù)全部寫上,要么把參數(shù)全部忽略不寫。如果傾向于寫出全部的參數(shù),則上面的例子該像下面這樣寫:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
如果想在表達(dá)式中使用參數(shù),則可以像下面代碼一樣使用:
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(view) -> presenter.onSaveClick(view, task)}"
- 還可以使用具有多個(gè)參數(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)聽的事件函數(shù)的返回值是非空的,則綁定表達(dá)式的值也必須返回相同類型的值。例如,正在監(jiān)聽
onLongClick()事件函數(shù),則綁定表達(dá)式需要返回boolean型的。如下所示:
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
- 如果由于空對(duì)象而無法計(jì)算綁定表達(dá)式的值,則 Data Binding 返回 Java 中默認(rèn)的值,例如:引用型對(duì)象則返回 null, int 型則返回0,Boolean 型則返回 false 等等。
- 如果需要使用帶斷言(例如三元表達(dá)式)的表達(dá)式,則可以使用void作為空操作符號(hào)。例如:
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免復(fù)雜的監(jiān)聽
- 監(jiān)聽器表達(dá)式是非常強(qiáng)大的,會(huì)讓你的代碼非常容易閱讀。
- 另一方面,如果監(jiān)聽器中包含復(fù)雜的表達(dá)式則會(huì)讓布局文件難以閱讀和維護(hù),所以布局文件中的表達(dá)式應(yīng)盡可能簡(jiǎn)單,表達(dá)式只是調(diào)用回調(diào)方法,而具體的業(yè)務(wù)邏輯應(yīng)該寫在回調(diào)方法中。
- 有個(gè)別點(diǎn)擊事件的監(jiān)聽器回調(diào)函數(shù)的方法名稱和 View 的
android:onClick相同,下面有一些新的屬性名稱,用于避免沖突:
| Class | Listener Setter | Attribute |
|---|---|---|
| SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
| ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
| ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
Data Binding 中的布局詳情
導(dǎo)入(Imports)
- 在布局文件中的
data標(biāo)簽中可以使用import標(biāo)簽導(dǎo)入類,就像在 java 文件中導(dǎo)入類一樣,如下所示:
<data>
<import type="android.view.View"/>
</data>
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
- 當(dāng)導(dǎo)入的類名沖突時(shí),可以使用
alias屬性為類起個(gè)別名,如下所示:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
- 別名只在此布局文件內(nèi)有效。導(dǎo)入的類,可以在 data binding 表達(dá)式中使用,也可以在申明變量時(shí)使用。
- 目前,在 Android Studio 中,并沒有提供 Data Binding 在布局文件中導(dǎo)入類自動(dòng)補(bǔ)全的功能。如果在布局文件中使用的類,沒有被導(dǎo)入,編譯可以正常通過,但是運(yùn)行時(shí)會(huì)出現(xiàn)問題??梢酝ㄟ^在申明變量時(shí),使用完全限定名類避免這個(gè)問題隱患。
- 在 data binding 表達(dá)式中可以使用導(dǎo)入類的靜態(tài)方法和靜態(tài)字段,如下所示:
<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 中一樣,
java.lang.*包下的類會(huì)被自動(dòng)導(dǎo)入。
變量(Variables)
- 在
data標(biāo)簽中,可以定義任意數(shù)量的變量,每個(gè)變量都可以被該布局文件中的任意一個(gè) data binding 表達(dá)式使用。如下所示:
<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>
- Data Binding 在編譯期內(nèi)會(huì)對(duì)申明的變量檢查類型,如果該變量實(shí)現(xiàn)了
Observable接口,或者是一個(gè)Observable集合類型的,那它應(yīng)該在類型上反映出來。若是一個(gè)沒有實(shí)現(xiàn)Observable的基礎(chǔ)類或者基礎(chǔ)接口,則該類不會(huì)被觀察。 - 自動(dòng)生成的 binding 類會(huì)為每個(gè)變量生成對(duì)應(yīng)的 setter 和 getter 方法,在每個(gè)變量的 setter 方法被調(diào)用之前,該變量將會(huì)采用默認(rèn)值,即:引用型變量默認(rèn)值是null, int 型變量默認(rèn)值是0,boolean 型變量的默認(rèn)值是 false。
- Data Binding 會(huì)生成一個(gè)特殊的名為
context的變量,以便 data binding 表達(dá)式在需要的時(shí)候使用,此 context 變量的值其實(shí)就是該布局文件中rootView的getContext()的返回值。 - 如果在該布局文件中有一個(gè)名為
context的變量,則 Data Binding 生成的context將會(huì)被覆蓋。
自定義 Binding 類的名稱(Custom Binding Class Names)
- Data Binding 會(huì)為每一個(gè)使用了 Data Binding 的布局文件生成一個(gè)對(duì)應(yīng)的 Binding 類,該類的名稱是基于布局文件的名稱的,采用大駝峰命名規(guī)則,移除下劃線_,并在最后追加 Binding,這個(gè)類會(huì)被放在該 Module 包的
databinding包中。例如:如果一個(gè)名為activity_main.xml的布局文件使用了 Data Binding 庫,則 Data Binding 庫會(huì)自動(dòng)生成一個(gè)名為ActivityMainBinding的 Binding 類,如果該 Module 的包名為com.lijiankun24.databindingpractice,則ActivityMainBinding類在com.lijiankun24.databindingpractice.databinding包下。 - 通過修改
data標(biāo)簽的class屬性,就可以修改 binding 類的名稱和位置。
- 若像下面這樣:
<data class="ActivityCustomBinding">
...
</data>
則該布局文件對(duì)應(yīng)的 Binding 類的名稱是 ActivityCustomBinding,而與該布局文件的名稱無關(guān)。
- 若像下面這樣:
<data class=".ActivityCustomBinding">
...
</data>
則該布局文件對(duì)應(yīng)的 Binding 類會(huì)被放在該 Module 包下,而不是該 Module 的 databinding 包下。
- 若像下面這樣:
<data class="com.example.ActivityCustomBinding">
...
</data>
則可以任意地指定該布局文件對(duì)應(yīng)的 Binding 類所在的位置。
Includes 標(biāo)簽
- 在
data標(biāo)簽中聲明的變量,可以通過應(yīng)用命名空間將該變量傳遞到被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>
如上面代碼所示,可以將在 data 標(biāo)簽中聲明的 user 變量傳遞到name.xml 和 contact.xml 布局文件中,前提是在這兩個(gè)布局文件中必須也聲明了 user 變量。
- Data Binding 庫并不支持
merge標(biāo)簽直接做為其子元素,如下所示的代碼是不允許的。
<?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>
表達(dá)式語法(Expression Language)
通用特性
表達(dá)式語言和 Java 表達(dá)式有很多相似之處,如下所示:
- 數(shù)學(xué)運(yùn)算符:+ - * / %
- 字符串連接符:+
- 邏輯運(yùn)算符:&& ||
- 位運(yùn)算符:& | ^
- 一元操作符:+ - ! ~
- 移位運(yùn)算: >> >>> <<
- 比較運(yùn)算符:== > < >= <=
- 實(shí)例判斷:instanceof
- 組:()
- Literals - character, String, numeric, null
- 類型轉(zhuǎn)換 Cast
- 方法調(diào)用 Method calls
- 字段存取 Field access
- 數(shù)組存取 Array access: []
- 三目運(yùn)算符:?:
如:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
不支持的操作符
一些在 Java 中的操作符,在 Binding 表達(dá)式中不支持,如下:
- this
- super
- new
- 顯式泛型調(diào)用
空合并運(yùn)算符(Null Coalescing Operator)
空合并運(yùn)算符(??): 如果左操作數(shù)不為空,則選擇左操作數(shù)否則選擇右操作數(shù)。
android:text="@{user.displayName ?? user.lastName}"
上面代碼等價(jià)于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
空指針異常處理(Avoiding NullPointerException)
Data Binding 生成的代碼中會(huì)自動(dòng)檢查 null 并避免空指針異常。例如,在 data binding 表達(dá)式 @{user.name} 中,如果 user 變量是 null 的,若 name 是 String 類型的,則將為 user.name 分配其默認(rèn)值 null;若引用了 user.age,其中 age 是 int 型的,那么它的默認(rèn)值是0。
集合(Collections)
可以使用 [] 操作符來操作通用的容器類,比如: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]}"
字符串語法(String Literals)
- 當(dāng)屬性值使用單引號(hào)括起來時(shí),在表達(dá)式中需要使用雙引號(hào)。
android:text='@{map["firstName"]}'
- 屬性值也可以使用雙引號(hào)括起來,則表達(dá)式中的字符串應(yīng)該使用 ' 或者后引號(hào) ` ,如:
android:text="@{map[`firstName`}"android:text="@{map['firstName']}"
資源(Resources)
- 在表達(dá)式中可以使用正常的語法引用資源。
android:padding="@{large ? @dimen/largePadding : @dimen/smallPadding}"
- 在字符串格式化和復(fù)數(shù)形式中可以使用參數(shù),如:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
- 當(dāng)復(fù)數(shù)形式中有多個(gè)參數(shù)是,多個(gè)參數(shù)必須同時(shí)傳遞進(jìn)去,如:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
- 一些資源需要在表達(dá)式中使用特定引用類型,如:
| Type | Normal Reference | Expression Reference |
|---|---|---|
| String[] | @array | @stringArrayint[] |
| array | @intArrayTypedArray | @array |
| typedArrayAnimator | @animator | @animatorStateListAnimator |
| animator | @stateListAnimatorcolor int | @color |
| colorColorStateList | @color | @colorStateList |
DataBinding 第一篇文章先介紹這些,如果有什么問題歡迎指出。我的工作郵箱:jiankunli24@gmail.com
參考資料:
深入Android Data Binding(一):使用詳解 -- YamLee
Android Data Binding 系列(一) -- 詳細(xì)介紹與使用 -- ConnorLin
DataBinding(一)-初識(shí) -- sakasa(譯)Data Binding 指南 -- 楊輝