寫在前面
要學(xué)習(xí)新東西,最好的辦法是先學(xué)會(huì)如何使用。所以,本文僅作 Android Data Binding 的介紹并結(jié)合 DataBindingDemo 來(lái)理解它的用法,后續(xù)再對(duì)其原理進(jìn)行深入探討。
簡(jiǎn)介
Data binding 在2015年7月發(fā)布的Android Studio v1.3.0 版本上引入,在2016年4月Android Studio v2.0.0 上正式支持。目前為止,Data Binding 已經(jīng)支持雙向綁定了。
Databinding 是一個(gè)實(shí)現(xiàn)數(shù)據(jù)和UI綁定的框架,是一個(gè)實(shí)現(xiàn) MVVM 模式的工具,有了 Data Binding,在Android中也可以很方便的實(shí)現(xiàn)MVVM開發(fā)模式。
Data Binding 是一個(gè)support庫(kù),最低支持到Android 2.1(API Level 7+)。
Data Binding 之前,我們不可避免地要編寫大量的毫無(wú)營(yíng)養(yǎng)的代碼,如 findViewById()、setText(),setVisibility(),setEnabled() 或 setOnClickListener() 等,通過(guò) Data Binding , 我們可以通過(guò)聲明式布局以精簡(jiǎn)的代碼來(lái)綁定應(yīng)用程序邏輯和布局,這樣就不用編寫大量的毫無(wú)營(yíng)養(yǎng)的代碼了。
構(gòu)建環(huán)境
-
首先,確保能使用Data Binding,需要下載最新的 Support repository。否則可能報(bào)錯(cuò),如圖:
-
在模塊的build.gradle文件中添加dataBinding配置
android { .... dataBinding { enabled = true } }注意:如果app依賴了一個(gè)使用 Data Binding 的庫(kù),那么app module 的 build.gradle 也必須配置 Data Binding。
Data Binding 布局文件 - (View)
Data binding 的布局文件與傳統(tǒng)布局文件有一點(diǎn)不同。它以一個(gè) layout 標(biāo)簽作為根節(jié)點(diǎn),里面是 data 標(biāo)簽與 view 標(biāo)簽。view 標(biāo)簽的內(nèi)容就是不使用 Data Binding 時(shí)的普通布局文件內(nèi)容。以下是一個(gè)例子:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<!-- 變量user, 描述了一個(gè)布局中會(huì)用到的屬性 -->
<variable name="user" type="com.connorlin.databinding.model.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}"/>
<!-- 布局文件中的表達(dá)式使用 “@{}” 的語(yǔ)法 -->
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
數(shù)據(jù)對(duì)象 - (Model)
假設(shè)你有一個(gè) plain-old Java object(POJO) 的 User 對(duì)象。
public class User {
private final String mFirstName;
private final String mLastName;
private int mAge;
public User(String firstName, String lastName, int age) {
mFirstName = firstName;
mLastName = lastName;
mAge = age;
}
}
或者是 JavaBean 對(duì)象:
public class User {
private final String mFirstName;
private final String mLastName;
private int mAge;
public User(String firstName, String lastName, int age) {
mFirstName = firstName;
mLastName = lastName;
mAge = age;
}
public String getFirstName() {
return mFirstName;
}
public String getLastName() {
return mLastName;
}
public int getAge() {
return mAge;
}
}
從 Data Binding 的角度看,這兩個(gè)類是一樣的。用于 TextView 的 android:text 屬性的表達(dá)式@{user.firstName},會(huì)讀取 POJO 對(duì)象的 firstName 字段以及 JavaBeans 對(duì)象的 getFirstName()方法。
綁定數(shù)據(jù) - (ViewModel)
在默認(rèn)情況下,會(huì)基于布局文件生成一個(gè)繼承于 ViewDataBinding 的 Binding 類,將它轉(zhuǎn)換成帕斯卡命名并在名字后面接上Binding。例如,布局文件叫 main_activity.xml,所以會(huì)生成一個(gè) MainActivityBinding 類。這個(gè)類包含了布局文件中所有的綁定關(guān)系,會(huì)根據(jù)綁定表達(dá)式給布局文件賦值。在 inflate 的時(shí)候創(chuàng)建 binding 的方法如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ActivityBaseBinding 類是自動(dòng)生成的
ActivityBaseBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_base);
User user = new User("Connor", "Lin");
// 所有的 set 方法也是根據(jù)布局中 variable 名稱生成的
binding.setUser(user);
}
事件處理
本部分源碼請(qǐng)參考 DataBindingDemo -> EventActivity 部分。
類似于 android:onClick 可以指定 Activity 中的函數(shù),Data Binding 也允許處理從視圖中發(fā)送的事件。
有兩種實(shí)現(xiàn)方式:
- 方法調(diào)用
- 監(jiān)聽綁定
二者主要區(qū)別在于方法調(diào)用在編譯時(shí)處理,而監(jiān)聽綁定于事件發(fā)生時(shí)處理。
方法調(diào)用
相較于 android:onClick ,它的優(yōu)勢(shì)在于表達(dá)式會(huì)在編譯時(shí)處理,如果函數(shù)不存在或者函數(shù)簽名不對(duì),編譯將會(huì)報(bào)錯(cuò)。
以下是個(gè)例子:
public class EventHandler {
private Context mContext;
public EventHandler(Context context) {
mContext = context;
}
public void onClickFriend(View view) {
Toast.makeText(mContext, "onClickFriend", Toast.LENGTH_LONG).show();
}
}
表達(dá)式如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handler"
type="com.connorlin.databinding.handler.EventHandler"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handler::onClickFriend}"/>
<!-- 注意:函數(shù)名和監(jiān)聽器對(duì)象必須對(duì)應(yīng) -->
<!-- 函數(shù)調(diào)用也可以使用 `.` , 如handler.onClickFriend , 不過(guò)已棄用 -->
</LinearLayout>
</layout>
監(jiān)聽綁定
監(jiān)聽綁定在事件發(fā)生時(shí)調(diào)用,可以使用任意表達(dá)式
此功能在 Android Gradle Plugin version 2.0 或更新版本上可用.
在方法引用中,方法的參數(shù)必須與監(jiān)聽器對(duì)象的參數(shù)相匹配。在監(jiān)聽綁定中,只要返回值與監(jiān)聽器對(duì)象的預(yù)期返回值相匹配即可。
以下是個(gè)例子:
public void onTaskClick(Task task) {
task.run();
}
表達(dá)式如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handler" type="com.connorlin.databinding.handler.EventHandler"/>
<variable
name="task" type="com.connorlin.databinding.task.Task"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> handler.onTaskClick(task)}"/>
</LinearLayout>
</layout>
當(dāng)一個(gè)回調(diào)函數(shù)在表達(dá)式中使用時(shí),數(shù)據(jù)綁定會(huì)自動(dòng)為事件創(chuàng)建必要的監(jiān)聽器并注冊(cè)監(jiān)聽。
關(guān)于參數(shù)
- 參數(shù)有兩種選擇:要么不寫,要么就要寫全。
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> handler.onTaskClick(task)}" />
或
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{(view) -> handler.onTaskClick(task)}"/>
- lambda 表達(dá)式可添加一個(gè)或多個(gè)參數(shù),同時(shí)參數(shù)可任意命名
public class EventHandler {
public void onTaskClickWithParams(View view, Task task) {
task.run();
}
}
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{(theview) -> handler.onTaskClickWithParams(theview, task)}" />
或者
public class EventHandler {
public void onCompletedChanged(Task task, boolean completed) {
if(completed) {
task.run();
}
}
}
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> handler.onCompletedChanged(task, isChecked)}" />
表達(dá)式結(jié)果有默認(rèn)值 null、0、false等等
表達(dá)式中可以使用void
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}" />
關(guān)于表達(dá)式
復(fù)雜的表達(dá)式會(huì)使布局難以閱讀和維護(hù),這種情況我們最好將業(yè)務(wù)邏輯寫到回調(diào)函數(shù)中
也有一些特殊的點(diǎn)擊事件 我們需要使用不同于 android:onClick 的屬性來(lái)避免沖突。
下面是一些用來(lái)避免沖突的屬性:
| Class | Listener Setter | Attribute |
|---|---|---|
| SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
| ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
| ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
布局詳情
本部分源碼請(qǐng)參考 DataBindingDemo -> CombineActivity 部分
導(dǎo)入(Imports)
- data 標(biāo)簽內(nèi)可以有多個(gè) import 標(biāo)簽。你可以在布局文件中像使用 Java 一樣導(dǎo)入引用
<data>
<import type="android.view.View"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
- 當(dāng)類名發(fā)生沖突時(shí),可以使用 alias
<import type="android.view.View"/>
<import type="com.connorlin.databinding.ui.View" alias="AliasView"/>
- 導(dǎo)入的類型也可以用于變量的類型引用和表達(dá)式中
<data>
<import type="com.connorlin.databinding.model.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
注意:Android Studio 還沒(méi)有對(duì)導(dǎo)入提供自動(dòng)補(bǔ)全的支持。你的應(yīng)用還是可以被正常編譯,要解決這個(gè)問(wèn)題,你可以在變量定義中使用完整的包名。
- 導(dǎo)入也可以用于在表達(dá)式中使用靜態(tài)方法
public class MyStringUtils {
public static String capitalize(final String word) {
if (word.length() > 1) {
return String.valueOf(word.charAt(0)).toUpperCase() + word.substring(1);
}
return word;
}
}
<data>
<import type="com.connorlin.databinding.utils.MyStringUtils"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
</data>
…
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
- java.lang.* 包中的類會(huì)被自動(dòng)導(dǎo)入,可以直接使用,例如, 要定義一個(gè) String 類型的變量
<variable name="test" type="String" />
變量 Variables
- data 標(biāo)簽中可以有任意數(shù)量的 variable 標(biāo)簽。每個(gè) variable 標(biāo)簽描述了會(huì)在 binding 表達(dá)式中使用的屬性。
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
- 可以在表達(dá)式中直接引用帶 id 的 view,引用時(shí)采用駝峰命名法。
<TextView
android:id="@+id/first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={user.firstName}" />
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{firstName.getVisibility() == View.GONE ? View.GONE : View.VISIBLE}" />
<!-- 這里TextView直接引用第一次TextView,firstName為id 的駝峰命名 -->
- binding 類會(huì)生成一個(gè)命名為 context 的特殊變量(其實(shí)就是 rootView 的 getContext() ) 的返回值),這個(gè)變量可用于表達(dá)式中。 如果有名為 context 的變量存在,那么生成的這個(gè) context 特殊變量將被覆蓋。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{handler.loadString(context)}"/>
public String loadString(Context context) {
// 使用生成的context變量
return context.getResources().getString(R.string.string_from_context);
}
自定義綁定類名
默認(rèn)情況下,binding 類的名稱取決于布局文件的命名,以大寫字母開頭,移除下劃線,后續(xù)字母大寫并追加 “Binding” 結(jié)尾。這個(gè)類會(huì)被放置在 databinding 包中。舉個(gè)例子,布局文件 contact_item.xml 會(huì)生成 ContactItemBinding 類。如果 module 包名為 com.example.my.app ,binding 類會(huì)被放在 com.example.my.app.databinding 中。
通過(guò)修改 data 標(biāo)簽中的 class 屬性,可以修改 Binding 類的命名與位置。舉個(gè)例子:
<data class="CustomBinding">
...
</data>
以上會(huì)在 databinding 包中生成名為 CustomBinding 的 binding 類。如果需要放置在不同的包下,可以在前面加 “.”:
<data class=".CustomBinding">
...
</data>
這樣的話, CustomBinding 會(huì)直接生成在 module 包下。如果提供完整的包名,binding 類可以放置在任何包名中:
<data class="com.example.CustomBinding">
...
</data>
Includes
在使用應(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.connorlin.databinding.model.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/include"
app:user="@{user}"/>
</LinearLayout>
</layout>
需要注意, activity_combine.xml 與 include.xml 中都需要聲明 user 變量。
Data binding 不支持直接包含 merge 節(jié)點(diǎn)。舉個(gè)例子, 以下的代碼<font color = "red">不能正常運(yùn)行 </font>:
<?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.connorlin.databinding.model.User"/>
</data>
<merge>
<include layout="@layout/include"
app:user="@{user}"/>
</merge>
</layout>
表達(dá)式語(yǔ)言
通用特性
表達(dá)式語(yǔ)言與 Java 表達(dá)式有很多相似之處。下面是相同之處:
- 數(shù)學(xué)計(jì)算 + - / * %
- 字符串連接 +
- 邏輯 && ||
- 二進(jìn)制 & | ^
- 一元 + - ! ~
- 位移 >> >>> <<
- 比較 == > < >= <=
- instanceof
- 組 ()
- 字面量 - 字符,字符串,數(shù)字, null
- 類型轉(zhuǎn)換
- 函數(shù)調(diào)用
- 字段存取
- 數(shù)組存取 []
- 三元運(yùn)算符 ?:
例子:
<!-- 內(nèi)部使用字符串 & 字符拼接-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`Age :` + String.valueOf(user.age)}"/>
<!-- 三目運(yùn)算-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
在xml中轉(zhuǎn)義是不可避免的,如 : 使用“&&”是編譯不通過(guò)的,需要使用轉(zhuǎn)義字符 "&&"
附:常用的轉(zhuǎn)義字符
| 顯示結(jié)果 | 描述 | 轉(zhuǎn)義字符 | 十進(jìn)制 |
|---|---|---|---|
| 空格 | ? | ? | |
| < | 小于號(hào) | < | < |
| > | 大于號(hào) | > | > |
| & | 與號(hào) | & | & |
| " | 引號(hào) | " | " |
| ' | 撇號(hào) | ' | ' |
| × | 乘號(hào) | × | × |
| ÷ | 除號(hào) | ÷ | ÷ |
不支持的操作符
一些 Java 中的操作符在表達(dá)式語(yǔ)法中不能使用。
- this
- super
- new
- 顯式泛型調(diào)用
<T>
Null合并運(yùn)算符
Null合并運(yùn)算符 ?? 會(huì)在非 null 的時(shí)候選擇左邊的操作,反之選擇右邊。
android:text="@{user.lastName ?? `Default LastName`}"
等同于
android:text="@{user.lastName != null ? user.lastName : `Default LastName`}"
容器類
通用的容器類:數(shù)組,lists,sparse lists,和 maps,可以用 [] 操作符來(lái)存取
<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]}"
字符串常量
使用單引號(hào)把屬性包起來(lái),就可以很簡(jiǎn)單地在表達(dá)式中使用雙引號(hào):
android:text='@{map["firstName"]}'
也可以用雙引號(hào)將屬性包起來(lái)。這樣的話,字符串常量就可以用 " 或者反引號(hào) ( ` ) 來(lái)調(diào)用
android:text="@{map[`firstName`}"
android:text="@{map["firstName"]}"
資源
也可以在表達(dá)式中使用普通的語(yǔ)法來(lái)引用資源:
android:text="@{@string/fullname(user.fullName)"
字符串格式化和復(fù)數(shù)形式可以這樣實(shí)現(xiàn):
android:text="@{@plurals/sample_plurals(num)}"
當(dāng)復(fù)數(shù)形式有多個(gè)參數(shù)時(shí),應(yīng)該這樣寫:
android:text="@{@plurals/numbers(num, num)}"
一些資源需要顯示類型調(diào)用。
| 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 |
數(shù)據(jù)對(duì)象 (Data Objects)
任何 POJO 對(duì)象都能用在 Data Binding 中,但是更改 POJO 并不會(huì)同步更新 UI。Data Binding 的強(qiáng)大之處就在于它可以讓你的數(shù)據(jù)擁有更新通知的能力。
有三種不同的動(dòng)態(tài)更新數(shù)據(jù)的機(jī)制:
- Observable 對(duì)象
- Observable 字段
- Observable 容器類
當(dāng)以上的 observable 對(duì)象綁定在 UI 上,數(shù)據(jù)發(fā)生變化時(shí),UI 就會(huì)同步更新。
Observable 對(duì)象
當(dāng)一個(gè)類實(shí)現(xiàn)了 Observable 接口時(shí),Data Binding 會(huì)設(shè)置一個(gè) listener 在綁定的對(duì)象上,以便監(jiān)聽對(duì)象字段的變動(dòng)。
Observable 接口有一個(gè)添加/移除 listener 的機(jī)制,但通知取決于開發(fā)者。為了簡(jiǎn)化開發(fā),Android 原生提供了一個(gè)基類 BaseObservable 來(lái)實(shí)現(xiàn) listener 注冊(cè)機(jī)制。這個(gè)類也實(shí)現(xiàn)了字段變動(dòng)的通知,只需要在 getter 上使用 Bindable 注解,并在 setter 中通知更新即可。
public class ObservableContact extends BaseObservable {
private String mName;
private String mPhone;
public ObservableContact(String name, String phone) {
mName = name;
mPhone = phone;
}
@Bindable
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
notifyPropertyChanged(BR.name);
}
@Bindable
public String getPhone() {
return mPhone;
}
public void setPhone(String phone) {
mPhone = phone;
notifyPropertyChanged(BR.phone);
}
}
BR 是編譯階段生成的一個(gè)類,功能與 R.java 類似,用 @Bindable 標(biāo)記過(guò) getter 方法會(huì)在 BR 中生成一個(gè) entry。
當(dāng)數(shù)據(jù)發(fā)生變化時(shí)需要調(diào)用 notifyPropertyChanged(BR.firstName) 通知系統(tǒng) BR.firstName 這個(gè) entry 的數(shù)據(jù)已經(jīng)發(fā)生變化以更新UI。
ObservableFields
創(chuàng)建 Observable 類還是需要花費(fèi)一點(diǎn)時(shí)間的,如果想要省時(shí),或者數(shù)據(jù)類的字段很少的話,可以使用 ObservableField 以及它的派生 ObservableBoolean、
ObservableByte 、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble、
ObservableParcelable 。
ObservableFields 是包含 observable 對(duì)象的單一字段。原始版本避免了在存取過(guò)程中做打包/解包操作。要使用它,在數(shù)據(jù)類中創(chuàng)建一個(gè) public final 字段:
public class ObservableFieldContact {
public ObservableField<String> mName = new ObservableField<>();
public ObservableField<String> mPhone = new ObservableField<>();
public ObservableFieldContact(String name, String phone) {
mName.set(name);
mPhone.set(phone);
}
}
要存取數(shù)據(jù),只需要使用 get() / set() 方法:
mObservableFieldContact.mName.set("ConnorLin");
mObservableFieldContact.mPhone.set("12345678901");
String name = mObservableFieldContact.mName.get();
Observable Collections 容器類
一些應(yīng)用會(huì)使用更加靈活的結(jié)構(gòu)來(lái)保持?jǐn)?shù)據(jù)。Observable 容器類允許使用 key 來(lái)獲取這類數(shù)據(jù)。當(dāng) key 是類似 String 的一類引用類型時(shí),使用 ObservableArrayMap 會(huì)非常方便。
ObservableArrayMap<String, String> mUser = new ObservableArrayMap<>();
mUser.put("firstName", "Connor");
mUser.put("lastName", "Lin");
mUser.put("age", "28");
mBinding.setUser(mUser);
在布局中,可以用 String key 來(lái)獲取 map 中的數(shù)據(jù):
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, String>"/>
</data>
…
<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"]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user["age"]}'/>
當(dāng) key 是整數(shù)類型時(shí),可以使用 ObservableArrayList :
ObservableArrayList<String> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add("17");
在布局文件中,使用下標(biāo)獲取列表數(shù)據(jù):
<data>
<import type="android.databinding.ObservableList"/>
<variable name="user" type="ObservableList<String>"/>
</data>
…
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{userList[0]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{userList[1]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{userList[2]}'/>
生成綁定
生成的 binding 類將布局中的 View 與變量綁定在一起。就像先前提到過(guò)的,類名和包名可以自定義 。生成的 binding 類會(huì)繼承 ViewDataBinding 。
Creating
binding 應(yīng)該在 inflate 之后創(chuàng)建,確保 View 的層次結(jié)構(gòu)不會(huì)在綁定前被干擾。綁定布局有好幾種方式。最常見的是使用 binding 類中的靜態(tài)方法。inflate 函數(shù)會(huì) inflate View 并將 View 綁定到 binding 類上。此外有更加簡(jiǎn)單的函數(shù),只需要一個(gè) LayoutInflater 或一個(gè) ViewGroup:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果布局使用不同的機(jī)制來(lái) inflate,則可以獨(dú)立做綁定操作:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有時(shí)綁定關(guān)系是不能提前確定的。這種情況下,可以使用 DataBindingUtil :
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
Views With IDs
布局中每一個(gè)帶有 ID 的 View,都會(huì)生成一個(gè) public final 字段。binding 過(guò)程會(huì)做一個(gè)簡(jiǎn)單的賦值,在 binding 類中保存對(duì)應(yīng) ID 的 View。這種機(jī)制相比調(diào)用 findViewById 效率更高。舉個(gè)例子:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.connorlin.databinding.model.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:id="@+id/firstName"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:id="@+id/lastName"/>
</LinearLayout>
</layout>
將會(huì)在 binding 類內(nèi)生成:
public final TextView firstName;
public final TextView lastName;
ID 在 Data Binding 中并不是必需的,但是在某些情況下還是有必要對(duì) View 進(jìn)行操作。
Variables
每一個(gè)變量會(huì)有相應(yīng)的存取函數(shù):
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
并在 binding 類中生成對(duì)應(yīng)的 getters 和 setters:
public com.connorlin.databinding.model.User getUser();
public void setUser(com.connorlin.databinding.model.User user);
public Drawable getImage();
public void setImage(Drawable image);
public String getNote();
public void setNote(String note);
ViewStubs
本部分源碼請(qǐng)參考 DataBindingDemo -> ViewStubActivity 部分。
ViewStub 相比普通 View 有一些不同。ViewStub 一開始是不可見的,當(dāng)它們被設(shè)置為可見,或者調(diào)用 inflate 方法時(shí),ViewStub 會(huì)被替換成另外一個(gè)布局。
因?yàn)?ViewStub 實(shí)際上不存在于 View 結(jié)構(gòu)中,binding 類中的類也得移除掉,以便系統(tǒng)回收。因?yàn)?binding 類中的 View 都是 final 的,所以Android 提供了一個(gè)叫 ViewStubProxy 的類來(lái)代替 ViewStub 。開發(fā)者可以使用它來(lái)操作 ViewStub,獲取 ViewStub inflate 時(shí)得到的視圖。
但 inflate 一個(gè)新的布局時(shí),必須為新的布局創(chuàng)建一個(gè) binding。因此, ViewStubProxy 必須監(jiān)聽 ViewStub 的 ViewStub.OnInflateListener,并及時(shí)建立 binding。由于 ViewStub 只能有一個(gè) OnInflateListener,你可以將你自己的 listener 設(shè)置在 ViewStubProxy 上,在 binding 建立之后, listener 就會(huì)被觸發(fā)。
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout ...>
<ViewStub
android:id="@+id/view_stub"
android:layout="@layout/include"
... />
</LinearLayout>
</layout>
在 Java 代碼中獲取 binding 實(shí)例,為 ViewStubProy 注冊(cè) ViewStub.OnInflateListener 事件:
mActivityViewStubBinding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
mActivityViewStubBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
IncludeBinding viewStubBinding = DataBindingUtil.bind(inflated);
User user = new User("Connor", "Lin", 28);
viewStubBinding.setUser(user);
}
});
通過(guò) ViewStubProxy 來(lái) inflate ViewStub :
public void inflate(View view) {
if (!mActivityViewStubBinding.viewStub.isInflated()) {
mActivityViewStubBinding.viewStub.getViewStub().inflate();
}
}
此處
isInflated()和getViewStub()會(huì)標(biāo)紅,請(qǐng)不要擔(dān)心,這并不是錯(cuò)誤,是 ViewStubProxy 中的方法。
高級(jí)綁定
動(dòng)態(tài)變量
有時(shí)候,有一些不可知的 binding 類。例如,RecyclerView.Adapter 可以用來(lái)處理不同布局,這樣的話它就不知道應(yīng)該使用哪一個(gè) binding 類。而在 onBindViewHolder(VH, int) ) 的時(shí)候,binding 類必須被賦值。
在這種情況下,RecyclerView 的布局內(nèi)置了一個(gè) item 變量。 BindingHolder 有一個(gè) getBinding 方法,返回一個(gè) ViewDataBinding 基類。
public void onBindViewHolder(BindingHolder holder, int position) {
holder.getBinding().setVariable(BR.item, mItemList.get(position));
holder.getBinding().executePendingBindings();
}
以上,詳細(xì)請(qǐng)參考 DataBindingDemo -> MainActivity 部分(使用 RecyclerView 實(shí)現(xiàn))。
直接 binding
當(dāng)變量或者 observable 發(fā)生變動(dòng)時(shí),會(huì)在下一幀觸發(fā) binding。有時(shí)候 binding 需要馬上執(zhí)行,這時(shí)候可以使用 executePendingBindings()。
后臺(tái)線程
只要數(shù)據(jù)不是容器類,你可以直接在后臺(tái)線程做數(shù)據(jù)變動(dòng)。Data binding 會(huì)將變量/字段轉(zhuǎn)為局部量,避免同步問(wèn)題。
屬性設(shè)置
本部分源碼請(qǐng)參考 DataBindingDemo -> AttributeSettersActivity 部分。
當(dāng)綁定數(shù)據(jù)發(fā)生變動(dòng)時(shí),生成的 binding 類必須根據(jù) binding 表達(dá)式調(diào)用 View 的 setter 函數(shù)。Data binding 框架內(nèi)置了幾種自定義賦值的方法。
自動(dòng)設(shè)置屬性
對(duì)一個(gè) attribute 來(lái)說(shuō),Data Binding 會(huì)嘗試尋找對(duì)應(yīng)的 setAttribute 函數(shù)。屬性的命名空間不會(huì)對(duì)這個(gè)過(guò)程產(chǎn)生影響,只有屬性的命名才是決定因素。
舉個(gè)例子,針對(duì)一個(gè)與 TextView 的 android:text 綁定的表達(dá)式,Data Binding會(huì)自動(dòng)尋找 setText(String) 函數(shù)。如果表達(dá)式返回值為 int 類型, Data Binding則會(huì)尋找 setText(int) 函數(shù)。所以需要小心處理函數(shù)的返回值類型,必要的時(shí)候使用強(qiáng)制類型轉(zhuǎn)換。
需要注意的是,Data Binding 在對(duì)應(yīng)名稱的屬性不存在的時(shí)候也能繼續(xù)工作。你可以輕而易舉地使用 Data Binding 為任何 setter “創(chuàng)建” 屬性。
如 DataBindingDemo 中的自定義布局 Card,并沒(méi)有添加 declare-styleable,但是可以使用自動(dòng) setter 的特性來(lái)調(diào)用這些函數(shù)。
<com.connorlin.databinding.view.Card
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:object="@{user}"/>
重命名屬性設(shè)置
一些屬性的命名與 setter 不對(duì)應(yīng)。針對(duì)這些函數(shù),可以用 BindingMethods 注解來(lái)將屬性與 setter 綁定在一起。舉個(gè)例子, android:tint 屬性可以這樣與 setImageTintList(ColorStateList) ) 綁定,而不是 setTint :
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
Android 框架中的 setter 重命名已經(jīng)在庫(kù)中實(shí)現(xiàn)了,我們只需要專注于自己的 setter。
自定義屬性設(shè)置
一些屬性需要自定義 setter 邏輯。例如,目前沒(méi)有與 android:paddingLeft 對(duì)應(yīng)的 setter,只有一個(gè) setPadding(left, top, right, bottom) 函數(shù)。結(jié)合靜態(tài) binding adapter 函數(shù)與 BindingAdapter 注解可以讓開發(fā)者自定義屬性 setter。
Android 屬性已經(jīng)內(nèi)置一些 BindingAdapter。例如,這是一個(gè) paddingLeft 的自定義 setter:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding adapter 在其他自定義類型上也很好用。舉個(gè)例子,一個(gè) loader 可以在非主線程加載圖片。
當(dāng)存在沖突時(shí),開發(fā)者創(chuàng)建的 binding adapter 會(huì)覆蓋 Data Binding 的默認(rèn) adapter。
你也可以創(chuàng)建多個(gè)參數(shù)的 adapter:
// 無(wú)需手動(dòng)調(diào)用此函數(shù)
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Glide.with(view.getContext()).load(url).error(error).into(view);
}
<!-- 當(dāng)url存在時(shí),會(huì)自動(dòng)調(diào)用注解方法,即loadImage()-->
<ImageView
app:imageUrl=“@{url}”
app:error=“@{@drawable/ic_launcher}”/>
當(dāng) imageUrl 與 error 存在時(shí)這個(gè) adapter 會(huì)被調(diào)用。imageUrl 是一個(gè) String,error 是一個(gè) Drawable。
- 在匹配時(shí)自定義命名空間會(huì)被忽略
- 你可以為 android 命名空間編寫 adapter
Binding adapter 方法可以獲取舊的賦值。只需要將舊值放置在前,新值放置在后:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
事件 handlers 僅可用于只擁有一個(gè)抽象方法的接口或者抽象類。例如:
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
當(dāng) listener 內(nèi)置多個(gè)函數(shù)時(shí),必須分割成多個(gè) listener。例如, View.OnAttachStateChangeListener 內(nèi)置兩個(gè)函數(shù): onViewAttachedToWindow()與 onViewDetachedFromWindow() 。在這里必須為兩個(gè)不同的屬性創(chuàng)建不同的接口。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因?yàn)楦淖円粋€(gè) listener 會(huì)影響到另外一個(gè),我們必須編寫三個(gè)不同的 adapter,包括修改一個(gè)屬性的和修改兩個(gè)屬性的。
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
上面的例子比普通情況下復(fù)雜,因?yàn)?View 是 add/remove View.OnAttachStateChangeListener 而不是 set。 android.databinding.adapters.ListenerUtil可以用來(lái)輔助跟蹤舊的 listener 并移除它。
對(duì)應(yīng) addOnAttachStateChangeListener(View.OnAttachStateChangeListener) )支持的 api 版本,
通過(guò)向 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1) 注解,
Data Binding 代碼生成器會(huì)知道這些 listener 只會(huì)在 Honeycomb MR1 或更新的設(shè)備上使用。
轉(zhuǎn)換器Converters
對(duì)象轉(zhuǎn)換
當(dāng) binding 表達(dá)式返回對(duì)象時(shí),會(huì)選擇一個(gè) setter(自動(dòng) Setter,重命名 Setter,自定義 Setter),將返回對(duì)象強(qiáng)制轉(zhuǎn)換成 setter 需要的類型。
下面是一個(gè)使用 ObservableMap 保存數(shù)據(jù)的例子:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在這里, userMap 會(huì)返回 Object 類型的值,而返回值會(huì)被自動(dòng)轉(zhuǎn)換成 setText(CharSequence) 需要的類型。當(dāng)對(duì)參數(shù)類型存在疑惑時(shí),開發(fā)者需要手動(dòng)做類型轉(zhuǎn)換。
自定義轉(zhuǎn)換
有時(shí)候會(huì)自動(dòng)在特定類型直接做類型轉(zhuǎn)換。例如,當(dāng)設(shè)置背景的時(shí)候:
<View
android:background="@{isError.get() ? @color/colorAccent : @color/colorPrimary}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在這里,背景需要的是 Drawable ,但是 color 是一個(gè)整數(shù)。當(dāng)需要 Drawable 卻返回了一個(gè)整數(shù)時(shí), int 會(huì)自動(dòng)轉(zhuǎn)換成 ColorDrawable 。這個(gè)轉(zhuǎn)換是在一個(gè) BindingConversation 注解的靜態(tài)函數(shù)中實(shí)現(xiàn):
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
需要注意的是,這個(gè)轉(zhuǎn)換只能在 setter 階段生效,所以 不允許 混合類型:
<View
android:background="@{isError.get() ? @drawable/error : @color/colorPrimary}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Android Studio對(duì)Data Binding的支持
-
Android Studio 支持 Data Binding 表現(xiàn)為:
- 語(yǔ)法高亮
- 標(biāo)記表達(dá)式語(yǔ)法錯(cuò)誤
- XML 代碼補(bǔ)全
- 跳轉(zhuǎn)到聲明或快速文檔
注意:數(shù)組和泛型類型,如 Observable 類,當(dāng)沒(méi)有錯(cuò)誤時(shí)可能會(huì)顯示錯(cuò)誤。
- 在預(yù)覽窗口可顯示 Data Binding 表達(dá)式的默認(rèn)值。例如:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=FirstName}"/>
<!-- TextView 的 text 默認(rèn)值為 FirstName -->
如果你需要在設(shè)計(jì)階段顯示默認(rèn)值,你可以使用 tools 屬性代替默認(rèn)值表達(dá)式,詳見 設(shè)計(jì)階段布局屬性
參考資料
我的簡(jiǎn)書賬號(hào)是 ConnorLin,歡迎關(guān)注!
我的簡(jiǎn)書專題是 Android開發(fā)技術(shù)分享,歡迎關(guān)注!