# 背景
略。
# 環境設置
要開始使用databinding,需要在module里面的build.gradle配置好相關的環境:
具體參照以下代碼:
android {
....
dataBinding {
enabled = true
}
}
如果一個library module設置了databinding,那么app module也*必須* 在build.gradle加上以上代碼。
# 在布局文件中使用databinding
## 示例代碼
數據綁定布局文件和一般布局文件略有不同,它以`<layout>`的根標簽開始,后跟`<data>`標簽和*視圖根元素*,即一般的視圖布局。示例文件如下所示:
<?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>`標簽里面的`<variable>`標簽聲明好需要綁定的數據,后面還可以看到`<import>`等標簽的使用。
## 綁定語法
* 數據綁定
綁定數據的表達式通過 `@{}` 語法寫入標簽的屬性里面,例如:`android:text="@{user.firstName}"`這句代碼就是把user.firstName設置到`android:text`屬性里面。
`@{}`里面的表達式,如果是對象引用,例如`user.firstName`,表達式求值的時候,會首先查找對象的getter方法,即`getFirstName()`方法。如果存在此方法,則以返回值作為表達式的值。如果不存在對應的getter,則會查找公開的字段,如果沒有相同命名的公開字段,編譯時就會報錯。
雙向綁定的語法是 `@={}` ,需要注意的是這是一項實驗性功能,尚未到正式版本。
* 事件處理
事件綁定語法和數據綁定差不多,都是通過 `@{}` 把方法的引用([Java8的語法](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html))給到特定的屬性。
但相對于屬性,設置方法的語法還是需要留意一下:
* 基本語法:把一個lambda表達式或者通過方法引用語法`::`設置方法到事件屬性里面,例如最常見的點擊事件
android:onClick="@{(v) -> presenter.onClick()}"
或者是:
android:onClick="@{presenter::onClickView}"
使用`::`語法的時候,需要保證的是`pesenter.onClickView`方法擁有和對應的事件方法同樣的參數,即`onClickView`方法的定義必須是下面這樣:
public T onClickView(View view){
...
}
* 假如需要方法里面的參數,只要需要其中一個,都要把所有參數寫上:
app:onTagClickListener="@{(view,tag,position) -> presenter.addListItem(position)}"
* 假如不需要任何方法里面的參數,那么括號里面的參數列表可以省略:
app:onTagClickListener="@{() -> presenter.onLoadMore()}"
* 如果方法需要有返回值,那么你的表達式或者方法的返回類型一定要和屬性的返回類型一致,否則會編譯不通過。
## 其他布局細節
### Import導包
`<data>`標簽里面還支持import語法,具體的格式是下面這樣,跟java代碼里面差不多:
<data>
<import type="android.view.View"/>
</data>
導包之后就可以使用它的一些靜態方法或者靜態變量,例如`View.GONE`等等,甚至可以導入`TextUtils`來判斷字符串非空。但由于可讀性和可維護性方面的問題,我們在項目中應該**禁止**xml中使用任何條件語句。
關于*import*更詳細的資料請查看[官方文檔](https://developer.android.com/topic/libraries/data-binding/index.html#imports)
### Variables變量
`variable`標簽會為每個聲明的變量賦初始值,如果是引用類型則初始值為null,int類型是0,等等。
xml中有一個隱藏的變量名`context`,它對應的值是布局根元素的`getContext`方法獲取的上下文對象。如果你的xml中顯式聲明`context`變量,那么這個變量會被你的聲明所覆蓋。
關于*variable*更詳細的資料請查看[官方文檔](https://developer.android.com/topic/libraries/data-binding/index.html#variables)。
### 自定義生成的綁定類名
默認情況下,根據布局文件的名稱生成一個Binding類,以大寫字母開頭,刪除下劃線`_`并大寫后面的字母,然后后綴“Binding”。 該類將放置在模塊包下的數據綁定包中。 例如,布局文件contact_item.xml將生成ContactItemBinding。 如果模塊包是com.example.my.app,那么它將被放置在com.example.my.app.databinding中。
綁定類可以通過調整`<data>`元素的`class`屬性來重命名或放置在不同的包中。 例如:
<data class="ContactItem">
...
</data>
以上設置會把xml生成的對應綁定類重命名為ContactItem,而不是XXXBinding。`class`屬性還可以設置相對或者完整包名,具體請查看[官方文檔](https://developer.android.com/topic/libraries/data-binding/index.html#custom_binding_class_names)
### Inclub標簽綁定
略。
[官方文檔](https://developer.android.com/topic/libraries/data-binding/index.html#includes)。
### 布局中可以使用的表達語言
從可讀性和可維護性的角度觸發,我們項目中禁止布局文件中做任何邏輯和業務相關的表達式運算,綁定的字段一般就是最終的輸出。作為了解,可以查看一下[官方文檔](https://developer.android.com/topic/libraries/data-binding/index.html#expression_language)。
# 綁定的數據對象
從上面的介紹得知,我們可以把普通的java對象(POJO)或者java bean對象(BEAN)綁定到屬性里面,但是當這些對象的屬性發生變化的時候,頁面是不會隨之發送變化的。如果需要屬性變化馬上反應到視圖上,就要使用綁定庫提供的`Data Objects`。有以下三種實現方法提供:
* 繼承BaseObservable,為需要實時變更的get方法加上`@Bindable`注解,編譯器會自動為該方法生成對應的id存放在BR里面(類似R文件)。對應的set方法需要調用`notifyPropertyChanged(id)`,例如:
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);
}
}
* 使用ObservableField和他的同胞 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.使用它們來替代你的類中需要實時變化的字段類型,不需要額外設置get set方法,但修飾符需要是`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();
}
* 使用Observable Collections,例如 ObservableArrayMap 和 ObservableArrayList。在xml中可以使用符號[]訪問。但直接使用集合來綁定數據,還是會破壞可讀性。在類中使用的話和ObservableField無異。所以有需要了解的請查看[官方文檔](https://developer.android.com/topic/libraries/data-binding/index.html#observable_collections)。
# 生成的綁定類淺析
前面已經介紹過,編譯器會為每一個使用數據綁定的xml生成一個繼承`android.databinding.ViewDataBinding`的綁定類。綁定類處理好所有該布局文件的屬性綁定和事件處理,并且提供對外的設置綁定對象的方法。這個類可以重命名,可以自定義包名等等,這些在之前已經描述過。
## 創建綁定
在布局被`inflate`之后應該盡快綁定,避免對View的操作造成綁定失敗。綁定的方法一般有以下幾種:
* 用生成的綁定類`inflate`,會同時執行好布局的`inflate`和綁定,無須額外操作:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
* 如果布局是使用不同的機制來進行`inflate`的,可以使用綁定類的`bind`方法進行綁定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
* 或者都可以直接用`DataBindingUtil.inflate`方法進行創建布局、生成綁定,相比具體的綁定類,只是多了參數,返回的類型是`<T extends ViewDataBinding>`:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
* 在Activity中可以使用`DataBindingUtil.setContentView`方法類代替activity原有的`setContentView`方法:
ActivityMain2Binding binding = DataBindingUtil.setContentView(activity.this, R.layout.activity_main2);
## 為帶id的元素生成引用
簡直就是**福音**!!!綁定類會為所有布局中帶id的元素生成引用,例如在xml中寫了一個TextView是id是`text`,綁定類里面就會生成一個`public final TextView text;`的引用,從此告別`findViewById`,我簡直都要哭了~生成的字段名的規則是首字母小寫,下劃線去除,下劃線后的首字母大寫。
## 為變量生成代碼
綁定類會為定義在`<variable>`標簽的對象生成對應的`get` `set`方法,如果有涉及到事件處理,還會根據事件傳入方式生成一些監聽器或者一些事件回調類:
// variables
@Nullable
private com.healthmall.library.app.databinding.DataBindingListItemInfoVM mItem;
@Nullable
private com.healthmall.library.app.databinding.ListPresenter mPresenter;
@Nullable
private final android.view.View.OnClickListener mCallback4;
// values
// listeners
private OnClickListenerImpl mPresenterOnClickViewAndroidViewViewOnClickListener;
## ViewStub綁定
提供了ViewStubProxy來解決ViewStub的綁定問題,具體請看[ViewStubProxy文檔](https://developer.android.com/reference/android/databinding/ViewStubProxy.html)。
## 高級特性
### 動態綁定
編譯器會為xml的每個變量生成對應的id,綁定類會為每個變量生成對應的get、set方法,但有時候我們沒辦法確定具體的id和具體的對象,譬如我們在編寫adapter基類,假設我們有一個統一的id `BR.item` ,但所有列表對象都基本不一致。所以需要使用ViewDataBinding(綁定類的基類)提供的`setVariable(bindId, bindObject)`方法來進行動態綁定。
@Override
public final void onBindViewHolder(BaseBindingViewHolder holder, int position) {
// 綁定數據
holder.bind.setVariable(BR.item, list.get(position));
holder.bind.executePendingBindings();
}
### 綁定立即生效
當變量或可觀察到的變化時,綁定將被安排在下一幀之前改變。 然而,有時候,綁定必須立即執行。 要強制執行,請使用`executePendingBindings`方法。
### 線程安全
只要不是集合,您可以在后臺線程中更改數據模型。 數據綁定將在評估時本地化每個變量/字段以避免任何并發問題。
# 屬性設置器
屬性設置器是編譯器連接代碼和布局的一套規則,它決定了綁定類如何生成。除了默認的規則以外,數據綁定框架還提供了自定義的方法,來幫助我們按照自己的規則去建立綁定。作為開發者而言,個人認為這是我們最應該熟悉的部分,沒有它上面的語法都不成立。
## 屬性自動設置器
對于綁定的任意一個屬性(忽略namespace),它會根據自己的屬性名去尋找對應的setter方法,而不管對應控件中是否已經聲明相同名稱的屬性字段。例如TextView里面的`android:text`屬性,關聯的表達式會被設置到`setText()`方法里面。例如我們的圖片顯示控件ThumbnailDraweeView,雖然沒有一個名為`imageURI`的屬性,但有`setImageURI`方法,所以我們可以輕松的使用`imageURI`屬性去為控件設置要顯示的圖片:
<com.gzdxjk.healthmall_android_library.widget.facebook.ThumbnailDraweeView
android:id="@+id/image"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="15dp"
app:imageURI="@{item.url}" />
有了這個機制以后,我們可以為任何setter方法在xml中創造對應的屬性,而不需要預先在控件中定義,包括事件處理,這也是極大的增加了靈活性。例如:
<com.gzdxjk.healthmall_android_library.widget.LoadingView
android:id="@+id/loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:noDataMessage="@{flowlist.noDataMsg}"
app:onRefreshListener="@{() -> presenter.onStart(loading, context, flowlist)}">
以上示例里面可以看到,我們項目中的LoadingView并沒有`noDataMessage`這個屬性,也沒有`onRefreshListener`這個屬性,但通過自動設置器,相應的表達式還是正確的設置到了對應的方法里面。
## 重命名設置器
有的屬性對應的setter方法會跟屬性名不一致,例如ImageView的`android:tint`屬性,它對應的方法是`setImageTintList`,而不是`setTint`。這時候可以使用`@BindingMethods`注解來重命名setter,它是一個類注解:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
## 自定義設置器
假如存在的屬性沒有對應的setter方法,那么可以編寫一個由`@BindingAdapter`注解的靜態方法來自定義該屬性的行為,例如`android:paddingLeft`屬性,并沒有一個`setPaddingLeft`方法讓自動設置器去設置它對應的表達式,這時候可以:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
假如自動設置器提供的方法不滿足你的需求,你也可以使用自定義設置器去覆蓋系統提供的方法。
BindingAdapter還可以接收多個參數,對應到一個方法里面。例如我們的流式布局FlowLayout中的自定義設置器:
@BindingAdapter({"bind:child_layout", "bind:items"})
public static void bindTextItem(FlowLayout flowLayout, int childLayoutId, List<String> itemList) {
flowLayout.addTextTag(childLayoutId, itemList);
}
當布局中的`child_layout`和`items`屬性同時存在(忽略namespace)并且它們的表達式返回類型和定義好的方法一致時,將會調用`bindTextItem`方法:
<com.gzdxjk.healthmall_android_library.widget.FlowLayout
android:id="@+id/flow_layout"
android:layout_width="match_parent"
android:layout_height="300dp"
app:child_layout="@{@layout/layout_textview}"
app:items="@{flowlist.list}"
app:onTagClickListener="@{(view,tag,position) -> presenter.addListItem(position)}" />
BindingAdapter還允許其處理方法使用變化前的值,具體的格式應該是舊值在前,新值在后:
@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());
}
}
對于一個接口有多個方法的情況,必須為每個方法重寫一個接口,并且在自定義設置器中一一對應,由于情況比較特殊,具體實例請查看[官方文檔](https://developer.android.com/topic/libraries/data-binding/index.html#custom_setters)。
# 數據類型轉換
同樣,為了可讀性考慮,禁止在xml中使用數據類型轉換。