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