本文建立在有一定使用 DataBinding 經驗的基礎之上,若還不熟悉 DataBinding 的用法妨退,請參考前一篇博客Data Binding 數(shù)據(jù)綁定(一)赠涮。
在學習 DataBinding 的過程中华弓,參考 Google 官方的 DataBinding 示例 Demo芭挽,自己寫了一個 DataBindingPractice Demo滑废,用于練手。整個工程采用 MVP 架構 + DataBinding袜爪,歡迎 star策严、fork 和溝通交流。
本文介紹了 DataBinding 一些稍微高級的用法饿敲,主要包括以下四部分內容:
- DataBinding 中的數(shù)據(jù)對象(Data Objects)
- DataBinding 中生成綁定類(Generated Binding)
- DataBinding 中的屬性設置(Attribute Setters)
- DataBinding 中的轉換器(Converters)
數(shù)據(jù)對象(Data Objects)
任何普通的 Java 對象(POJO)都可以被 DataBinding 所使用,但是改變 POJO 對象的屬性值并不會更新 UI 界面的顯示逛绵。DataBinding 真正強大之處在于怀各,它可以讓你的數(shù)據(jù)對象具有通知 UI 界面對象的屬性已經發(fā)生改變的能力。
有三種不同的數(shù)據(jù)變化通知機制:
- Observable objects
- observable fields
- observable collection
- 如果這其中的一種數(shù)據(jù)對象被綁定到 UI 界面上术浪,當數(shù)據(jù)對象的屬性值發(fā)生變化時瓢对,UI 界面會自動更新。
可觀察對象(Observable Objects)
- 一個類如果實現(xiàn)了
Observable
接口胰苏,那么 DataBinding 則會將一個listener
綁定到該類上硕蛹,就可以監(jiān)聽該類對象中的屬性的變化。Observable
接口具有添加和移除listener
的機制硕并,但是否通知則取決于開發(fā)者法焰。 - 為了使開發(fā)更容易,DataBinding 提供了一個名為
BaseObservable
的基類倔毙,它用于實現(xiàn)listener
注冊機制埃仪。 - 實現(xiàn)
Observable
的類負責什么時候通知該類的屬性發(fā)生了變化,只需要在類的getter
方法上添加Bindable
注解陕赃,并在setter
方法中通知更新即可卵蛉。
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
在編譯期間颁股,使用
Bindable
注解標記過的getter
方法會在BR
class 文件中生成一個入口,BR
class 文件是在 Module 的包下傻丝,BR.class
與R.class
的功能類似甘有。
可觀察屬性(ObservableFields)
- 創(chuàng)建一個
Observable
類還是需要一些工作量的,如果開發(fā)者不想花費太多的時間和精力葡缰,或者沒有太多的屬性需要觀察監(jiān)聽的話亏掀,那么可以使用ObservableField
,或者它的子類:ObservableBoolean
运准,ObservableByte
幌氮,ObservableChar
,ObservableShort
胁澳,ObservableInt
该互,ObservableLong
,ObservableFloat
韭畸,ObservableDouble
和ObservableParcelable
宇智。 -
ObservableField
是包含Observable Object
對象的單一字段。原始版本避免了在獲取過程中做打包和解包的操作胰丁。在數(shù)據(jù)對象中使用ObservableField
随橘,需要創(chuàng)建一個public final
字段,如下所示:
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
可以通過 set
方法和 get
方法存取數(shù)據(jù)
user.firstName.set("Google");
int age = user.age.get();
可觀察集合(Observable Collections)
- 一些應用會使用動態(tài)的結構持有數(shù)據(jù)锦庸,可觀察容器類允許使用鍵值對的形式來存取數(shù)據(jù)机蔗。
- 當鍵值對中的鍵是應用型數(shù)據(jù)(比如:String)時,
ObservableArrayMap
是非常有用的甘萧。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局文件中萝嘁,也可以通過使用 String 類型的鍵來獲取到相應的值。
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text='@{user["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user["age"])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
- 當鍵值對中的鍵是 Integer 型的扬卷,
ObservableArrayList
則是非常有用的牙言。
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在布局文件中,也可以通過使用 Integer 類型的鍵來獲取到相應的值怪得。
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
```
## 生成綁定類(Generated Binding)
1. 生成的綁定類通過布局文件中的 Views 和布局文件中的變量聯(lián)系起來咱枉。
2. 如之前所討論的那樣,綁定類的名稱和所在的位置都是可以自定義的徒恋。
3. 生成的所有的綁定類都是 `ViewDataBinding` 的子類蚕断。
### 構建(Creating)
1. 綁定類在 View Inflate 之后立即被創(chuàng)建,以確保在布局中的表達式被綁定到視圖之前入挣,View 的層次結構不會被打亂基括。
2. 有幾種方式綁定布局文件,最常用的是使用 Binding 類中的靜態(tài)方法來綁定類财岔。`inflate` 方法調用一次就可以 Inflate View 并將 View 綁定到 Binding 類上风皿。
3. 還有一個更加簡單的方法河爹,只需要一個 `LayoutInflater` 對象和一個 `viewGroup` 對象。
``` Java
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
- 如果布局使用另外不同的機制來 inflate桐款,則可以單獨綁定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
- 有時候咸这,Binding 類的名字不得而知,在這種情況下魔眨,則可以使用
DataBindingUtil
生成該 Binding 類:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
帶 ID 的 View(Views with IDs)
- 使用 DataBinding 庫的布局文件媳维,其中的每個帶 ID 的 View,編譯以后遏暴,都會在該布局文件對應的 Binding 類中生成一個被
public final
修飾的屬性侄刽,Data Binding 會做一個簡單的賦值,在 Binding 類中保存對應 ID 的 View朋凉。 - 通過這種機制獲取控件比通過
findViewById
獲取控件的速度會更快州丹。例如:
<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}"
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>
在生成的 Binding 類中會有對應的字段:
public final TextView firstName;
public final TextView lastName;
如果使用 DataBinding 庫的話,在布局文件中為控件設置
Id
不是必須的杂彭,但是在某些情況下墓毒,在代碼中通過Id
得到控件還是有必要的。
變量(Variables)
布局文件中的每個變量都會生成對應的存取方法亲怠,如:
<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>
在該布局文件對應的 Binding 類中所计,都會生成對應的存取方法,如下所示:
public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);
ViewStubs
- ViewStub 和普通的 View 相比是不一樣的团秽。它們最開始是不可見的主胧,當它們被設置為可見的或者調用
inflate
方法時,ViewStub 會被替換為另外一個控件或布局习勤。 - 因為最開始的時候讥裤,ViewStub 在布局層級中不可見,Binding 類中對應的控件也應該被移除姻报,以便回收。
- 因為在 Binding 類中间螟,所有 View 對應的屬性都是被
final
字段修飾的吴旋,所以一個ViewStubProxy
對象代替該 ViewStub,當 ViewStub 被設置為可見的或調用inflate
方法之后厢破,開發(fā)者可以通過此代理類ViewStubProxy
得到對應的 ViewStub荣瑟。 - 當
inflate
一個新的布局時,必須為新的布局創(chuàng)建新的 Binding 類摩泪。所以 ViewStubProxy 必須監(jiān)聽 ViewStub 的ViewStub.OnInflateListener
笆焰,當 ViewStub 被inflate
的時候,則建立一個新的 Binding 類见坑。 - 因為 ViewStub 只能設置一個
OnInflateListener
嚷掠,開發(fā)者可以為ViewStubProxy
設置一個OnInflateListener
捏检,在 Binding 類被建立以后,OnInflateListener
就會被觸發(fā)不皆。
代碼如下所示:
<layout>
...
<ViewStub
android:id="@+id/viewStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_view_stub"/>
...
</layout>
mBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
LayoutViewStubBinding mStubBinding = DataBindingUtil.findBinding(inflated);
mStubBinding.tvViewStub.setOnClickListener((view1 -> showClickToast()));
}
});
高級綁定(Advanced Binding)
動態(tài)變量(Dynamic Variables)
- 有時候贯城,一些 Binding 類不為人所知。比如霹娄,在
RecyclerView.Adapter
中可以用來處理不同的布局能犯,此時便不知道該 Binding 類具體是什么類型的。而在onBindViewHolder(VH, int)
方法中犬耻,ViewHolder 中的 Binding 類又必須被賦值踩晶。 - 在這個例子中,所有 RecyclerView 涉及到的布局中枕磁,都有一個
item
的變量渡蜻。 - Adapter 所使用的 ViewHolder 中有一個
getBinding
的方法得到一個ViewDataBinding
的 Binding 類。如下所示:
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
立即綁定(Immediate Binding)
當變量或者 observable
變量發(fā)生變化時透典,會在下一幀才觸發(fā) Binding晴楔,但是有時候需要立即 Binding,可以通過 executePendingBindings()
方法立即觸發(fā) Binding峭咒。
后臺線程
只要不是集合類型的數(shù)據(jù)税弃,你可以在后臺線程中更改數(shù)據(jù)。Data Binding 會在計算時將每個變量/字段在各個線程中做一份數(shù)據(jù)拷貝凑队,以避免同步問題则果。
屬性設置(Attribute Setters)
當一個屬性值發(fā)生變化時,生成的 Binding 類必須調用該控件對應 data binding 表達式的 setter
方法漩氨。Data Binding 框架允許自定義調用何種方法改變值西壮。
自動設置屬性(Automatic Setters)
- 對于一個屬性
attribute
,Data Binding 會嘗試著去找setAttribute
方法叫惊。屬性的命名空間是什么并沒有什么關系款青,只和屬性本身的名稱有關。例如霍狰,為 TextView 的屬性android:text
設置了一個 binding 表達式抡草,則 Data Binding 庫會去尋找setText(String)
的方法。 - 如果 data binding 表達式返回了一個 int 型數(shù)據(jù)蔗坯,Data Binding 則會去尋找
setText(int)
的方法康震。對于 data binding 表達式的返回值一定要小心處理,如果必要的話宾濒,需要做類型強制裝換腿短。 - 需要注意的是,就算給定名稱的屬性不存在,Data Binding也會生效橘忱。正是因為如此赴魁,使用 Data Binding 則可以方便地自定義屬性。例如鹦付,
DrawerLayout
控件并沒有什么屬性尚粘,但是卻有很多的setters
方法,就可以方便地使用自動設置屬性給DrawerLayout
設置屬性敲长。
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
重命名屬性設置(Renamed Setters)
有些屬性有其對應的 setter
方法郎嫁,但是該 setter
方法和其屬性名稱并不是那么相匹配。對于這些方法祈噪,可以使用 BindingMethods
注解將該屬性與對應的方法關聯(lián)起來泽铛。例如:屬性 android:tint
真正是和 setImageTintList(ColorStateList)
關聯(lián)起來的,而不是和 setTint
方法關聯(lián):
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
在 Android 框架中實現(xiàn)的屬性的 setter
方法已經不錯辑鲤,所以不需要開發(fā)者重命名屬性設置了盔腔。
自定義屬性設置(Custom Setters)
- 一些屬性需要自定義邏輯。例如月褥,沒有一個
setter
方法和屬性android:paddingLeft
相關聯(lián)弛随,但是卻存在setPadding(left, top, right, bottom)
方法。被BindingAdapter
注解修飾的靜態(tài) binding adapter 方法允許開發(fā)者自定義一個屬性的setter
方法如何被調用宁赤。
Android 已經內置了一些BindingAdapters
舀透。如下是一個與屬性paddingLeft
相關聯(lián)的setter
方法。
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
- Binding adapters 在其他自定義類型上也非常好用决左。
當開發(fā)者自定義的binding adapters
與默認的adapters
沖突時愕够,開發(fā)者自定義的會覆蓋默認的。
當然也可以自定義接收多個參數(shù)的adapters
佛猛,一個在非主線程中加載圖片的Loader
如下所示:
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext())
.load(url)
.error(error)
.into(view);
}
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>
```
如果在一個 ImageView 中 `imageUrl` 屬性和 `error` 屬性同時被使用惑芭,并且 `imageUrl` 是 String 類型的,`error` 屬性是 Drawable 類型的继找,則這個 `adapter` 將會被調用遂跟。
* 在匹配的過程中,自定義的命名空間將會被忽略
* 也可以為 android 命名空間編寫 adapter
3. `binding adapter` 中的方法可以獲取舊值婴渡,只需要將舊值放置在前幻锁,而新值放置在后,如下所示:
``` Java
@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
中缩搅,只可用于只擁有一個抽象方法的接口或抽象類,如下所示:
@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);
}
}
}
- 當一個
listener
中有多個方法時触幼,它必須拆分成多個listener
硼瓣。例如:View.OnAttachStateChangeListener
有兩個方法:onViewAttachedToWindow()
和onViewDetachedFromWindow()
。則必須為這兩個方法設置不同的屬性,分別處理其響應事件堂鲤。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因為改變一個 listener
必將會影響到另一個亿傅,所以我們必須有三個不同binding adapters
,包括修改一個屬性和修改兩個屬性的瘟栖,如下所示:
@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);
}
}
}
上面這個例子比正常情況下要更復雜一些葵擎,因為 View 是通過在代碼使用 add/remove 方法添加和移除
View.OnAttachStateChangeListener
,而不是通過setter
方法設置監(jiān)聽器的半哟。android.databinding.adapters.ListenerUtil
可以用來跟蹤之前的listener
酬滤,并可以在Binding Adaper
中移除監(jiān)聽器listener
。
通過向OnViewDetachedFromWindow
和OnViewAttachedToWindow
接口添加@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注解寓涨,Data Binding 代碼生成器知道監(jiān)聽器只在 Honeycomb MR1 設備或更新版本的設備中使用盯串。
轉換器(Converters)
對象轉換(Object Conversions)
當 binding 表達式返回一個對象時,一個 setter
方法(自動 Setter戒良,重命名 Setter体捏,自定義 Setter),并將返回的對象強制轉換成所選擇的 setter
方法所需要的類型糯崎。
以下是一個使用 ObservableMaps
持有數(shù)據(jù)并轉換的例子:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap
返回一個對象几缭,并且這個對象會被自動地轉換為 setter setText(CharSequence)
所需要的類型。當參數(shù)類型選擇存在疑惑時沃呢,需要開發(fā)者手動地將數(shù)據(jù)類型進行轉換年栓。
自定義類型轉換器(Custom Conversions)
有時候,屬性的值需要在特定類型之間自動轉換樟插。例如韵洋,在設置背景的時候:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在這里,背景需要 Drawable 類型的黄锤,但是顏色卻是 Integer 類型的搪缨。當需要一個 Drawable,binding 表達式返回的卻是 Integer 的鸵熟,所以此 int 型數(shù)據(jù)應該轉換成 ColorDrawable副编,此轉換可以通過一個被 BindingConversion
注解修飾的靜態(tài)方法完成。
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
需要注意的是流强,此轉換只能在 setter
階段完成痹届,所以它不允許如下面這樣混合類型的:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
DataBinding 第二篇文章也介紹完成。至此打月,關于 DataBinding 的文章到此就告一段落队腐。如果有什么問題歡迎指出。我的工作郵箱:jiankunli24@gmail.com
參考資料:
深入Android Data Binding(一):使用詳解 -- YamLee
Android Data Binding 系列(一) -- 詳細介紹與使用 -- ConnorLin
DataBinding(一)-初識 -- sakasa
(譯)Data Binding 指南 -- 楊輝