說明:本文是按照Android官方文檔順序進行部分翻譯,並結合自身實踐進行總結的,不保證100%還原官方內容,建議還是先看下官方的說明文檔
這篇文章將教你如何使用Data Binding Library來書寫聲明式的(declarative)佈局,以及使用儘可能少的代碼來使應用邏輯與佈局綁定。
Data Binding Library不僅靈活而且具有廣泛的兼容性,它是個支持庫,你可以應用到Android 2.1(API level 7+)之後的所有安卓平臺上。
使用此支持庫,需要使用1.5.0-alpha1或者更高版本的android gradle插件。
1 Build Environment - 構建環境
首先,我們需要從Android SDK Manager的Support repository中下載此庫。
然後,在app module下的build.gradle
文件中添加dataBinding
元素。代碼如下:
android {
....
dataBinding {
enabled = true
}
}
如果你使用的支持庫也使用了data binding,也需要在其build。gradle
文件下進行同樣的配置。
同時,要確保你使用的Android Studio是1.3及以上的版本
。
注:添加以上配置之後,Sync一下,然後項目會自動添加依賴的庫。
2 Data Binding Layout Files - Data Binding佈局文件
2.1 Data Binding表達式
Data-binding佈局文件稍有些不同,它的根佈局標籤爲layout
,包含一個data
元素和view
根元素,view
元素就是我們正常使用的佈局。舉例如下:activity_main.xml
<?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
內的user
變量包含了可能在接下來的佈局中被用到的屬性。
<variable name="user" type="com.example.User"/>
在layout中的表達式,用@{}
語句被寫在相應的屬性中。這裏的TextView的text展示就是user的fristName屬性值。
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
還可以做鏈式操作,user.firstName
得到的String
,我們可以繼續調用String
的相應方法
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName.toUpperCase()}"/>
遇到的坑
注意視圖的屬性對不同參數類型的處理有沒有區別,比如android:text
,在Databinding內部應該是調用了TextView的setText()
方法,如果@{}
表達式內是數字的話,例如@{user.age}
,會報資源找不到的錯誤(android.content.res.Resources$NotFoundException
),因此我們的表達式應該是@{String.valueOf(user.age)}
2.2 Data Object - Data對象
我們創建一個在上邊用到的數據對象
public class User {
private String firstName;
private String lastName;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName() {
return lastName;
}
public String getFirstName() {
return firstName;
}
}
在佈局文件中,TextView的android:text
屬性,使用表達式@{user.firstName}
將會訪問User類的firstName
屬性以及getFirstName()
方法,或者訪問firstName()
方法,如果它存在的話。
2.3 Binding Data - 綁定數據
默認情況下,Android Studio會自動根據以layout
作爲根佈局的文件名稱生產一個Binding類,比如上面的佈局文件activity_main
,生產的Binding類名稱爲ActivityMainBinding
,然後在MainActivity裏進行數據綁定:
Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
ActivityMainBinding下的方法,都是根據佈局文件中的variable
標籤的name
屬性自動生成的,因爲我們的佈局文件裏有個name爲user的variable,那麼就生成了setUser
方法,參數是variable
type對應的類。
運行程序後,你就會在界面上看到文字Test User。或者,你可以通過以下方式獲取:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
完整的代碼應該是這樣的:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
View view = binding.getRoot();
setContentView(view);
還有下面這種方式:
View root = getLayoutInflater().inflate(R.layout.activity_main, null);
setContentView(root);
ActivityMainBinding binding = ActivityMainBinding.bind(root);
2.4 Binding Events - 事件綁定
理解了上邊的數據綁定,事件綁定就好理解了,跟數據綁定類似。
以點擊事件爲例,聲明一個variable,名稱爲onClicklistener
,以MainActivity
作爲處理類
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user" type="com.chiemy.example.databindingexample.bean.User" />
<variable
name="onClicklistener"
type="com.chiemy.example.databindingexample.MainActivity"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_list_item_binding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ListItemBinding"
android:onClick="@{onClicklistener.onClick}"
/>
</LinearLayout>
</layout>
MainActivity下要實現佈局文件表達式中用到的方法,接收參數爲View:
public void onClick(View view){
//TODO 處理點擊事件
}
遇到的坑
使用Android Studio 2.1.1編譯測試,本來開始用起來沒有任何問題,但當我新建了一個Activity,再使用這種方式進行事件綁定時,問題出現了。在新的Activity的佈局文件中也採用如上方式,點擊按鈕時應用直接崩潰了,提示我Activity裏沒有聲明相應的方法(java.lang.IllegalStateException:
Could not find a method onClick(View) ......
),似乎對android:onClick
表達式的識別出了問題,但在Android
Studio 1.5.1上測試編譯沒有問題。如果你也遇到了同樣的問題並找到了解決辦法,請指點。
3 Layout Details - 佈局深入
我們可以在data
標籤裏使用import
元素,這樣我們可以像java一樣,簡單的導入一些類。
例如:
<data>
<import type="android.view.View"/>
</data>
現在我們就可以在binding表達式裏使用View了
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isFriend ? View.VISIBLE : View.GONE}"/>
實際測試(Android Studio 1.5.1),在佈局文件中這樣使用會提示Cannot resolve symbol
的錯誤,但是編譯和運行並沒有問題。
當類名有衝突的時候,我們可以使用alias:
屬性爲類起個別名,比如有個類com.example.real.estate.View
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
現在,我們使用Vista
引用的就是com.example.real.estate.View
類,View
引用的就是android.view.View
類了。
在variable
中,多次用到某個類的時候,import
也是很有用處的。類似於java中,如果不導包,我們在每次用到某個類時,都要寫類的全稱(包名+類名),導包後我們只需寫類名就可以了。
注:Android Studio還不支持類似java中的
import com.example.*
;
import的類型同時支持靜態變量和方法的表達式:
public class StringUtils {
public static String capitalize(String text){
return text.toUpperCase();
}
}
<data>
<import type="com.chiemy.example.databindingexample.StringUtils"/>
</data>
<TextView
android:text="@{StringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
3.1 Variables
在data
元素中可以有任意數量的variable
元素,佈局文件中的binding表達式可能會用到variable
元素所描述的屬性。
<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>
variable
類型會在編譯的時候被檢查,如果它實現了Observable
接口或者是一個observabel
collection
,應該反映到類型中。如果它是一個沒有實現Observabled的基本的類或接口,它就不會被觀察。
當對於不同配置(如,橫豎佈局)有不同的佈局文件時,variables將會被合併,因此不同的佈局之間不能存在衝突的variable定義。
生成的binding類,會爲每個variable提供一個getter和setter方法,直到調用setter方法時,variable纔會被設置Java的默認值,引用類型爲null,int類型爲0,boolean類型爲false,等等。
有個默認的名爲context的variable, 類型爲Context, 它是通過根佈局的getContext()方法得到的,我們可以直接使用
public class StringUtils {
public static String packageName(Context context){
return context.getPackageName();
}
}
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{StringUtils.packageName(context)}"
/>
3.2 Custom Binding Class Names - 自定義Binding類的名稱
默認情況下,Binding類的名稱是根據類名生成的,去除佈局名稱中的“_”,以駝峯命名的形式,並以Binding結尾。這個類將被放置在module包下的databinding包下。例如,contact_item.xml
將會生成ContactItemBinding
,如果module的包爲com.example.my.app
,那麼類所處的包爲com.example.my.app.databinding.
(但你是看不到的)。
通過data
元素的class
屬性,Binding類可以被重命名或者指定所在的包,例如:
<data class="ContactItem">
...
</data>
這個生成的Binding類名稱爲ContactItem
,位於module包下的databinding包中。
如果我們想指定它直接在module包下,我們可以在前面加個.
<data class=".ContactItem">
...
</data>
我們還可以指定其他包,但要注意包必須存在,不會自動生成。如我們的module包名爲com.example.app
, class可以是:
- com.example.ContactItem
- com.ContactItem
不能是不存在的包,如com.other.ContactItem
。
3.3 Includes
Variable也可以傳遞到一個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}"/>
</LinearLayout>
</layout>
注意,要用到xmlns:bind="http://schemas.android.com/apk/res-auto"
命名空間的聲明。
同時,include的佈局文件裏,必須包含跟傳遞的variable相同的variable。
Data binding不支持include一個以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>
3.4 Expression Language - 表達式語言
Common Features - 通用屬性
許多和Java表達式相同:
- 數學運算符
+ - / * %
- 字符連接
+
- 邏輯運算
&& ||
- 位運算符
& | ^
- 一元運算符
+ - ! ~
- 位移運算
>> >>> <<
- 比較
== > < >= <=
- instanceof
- Grouping ()
- Literals - character, String, numeric, null
- 轉型
- 方法調用
- 屬性訪問
- 數組訪問
[ ]
- 三目運算符
舉例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
Missing Operations - 沒有的操作
- this
- super
- new
- 顯式泛型調用
Null Coalescing Operator - Null合併操作
選擇不爲空的值
android:text="@{user.displayName ?? user.lastName}"
與以下三目運算等價
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
Avoiding NullPointerException - 空指針安全
生成的data binding代碼自動檢驗null值,並避免空指針的發生。例如在@{user.name}
表達式中,如果user是null的,user.name將會取默認值null,如果你引用user.age,age是int型,那麼值將會是0。
Collections - 集合
通用的容器:數組、List、SparseArray、Map,可以通過[ ]
方便的訪問。
<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 - String迭代
當屬性值用單引號包裹時,表達式內部用雙引號。
android:text='@{map["firstName"]}'
也可以屬性值用雙引號包裹,表達式內使用"
或者反單引號(`)
android:text="@{map[`firstName`}"
android:text="@{map["firstName"]}"
Resources - 資源
也可以在表達式中使用正常的語法訪問資源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式化的和複數的String,可以根據提供的參數進行匹配。
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
正常引用和表達式的對應關係如下:
類型 | 正常引用 | 表達式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
4 Data Objects - 數據對象
POJO可以用於data dinding,但是修改POJO並不會引起UI的更新。data binding的強大之處在於賦予你的數據對象當數據變化時去更新UI的能力。有三種不同的數據通知更新的機制,Observable objects, observable fileds,以及observable collections。
4.1 Observable Objects
實現Observable接口的類,允許監聽器屬性的變化。
Observable
接口有添加和移除監聽的能力,但是通知則依賴於開發者。爲了使開發簡單,BaseObservable
類,已經實現了監聽註冊的機制。實現類還是得在屬性變化的時候負責提醒。通過在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);
}
}
4.2 ObservableFields
像上邊的方式,我們有一部分工作花在了創建Observable
類上,如果我們想節省時間,或者我們只有很少的屬性,我們可以使用ObservableField
,以及它的弟兄們- ObservableBoolean
, ObservableByte
, ObservableChar
,ObservableShort
, ObservableInt
, ObservableLong
, ObservableFloat
, ObservableDouble
,ObservableParcelable
。ObservableField
自己保存只有一個屬性的observable對象,早期的版本在訪問時會避免自動裝箱和拆箱。使用方式如下:
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
設置和獲取屬性的時候用以下方式:
user.firstName.set("Google");
int age = user.age.get();
遇到的坑
本想將ObservableField
及相關的屬性設置爲私有的,然後簡化getter方法,像下邊這樣:
public class ObservableFiledsUser {
private ObservableInt age = new ObservableInt();
public void setAge(int age) {
this.age.set(age);
}
public int getAge() {
return age.get();
}
}
但是這樣做不會引起視圖的自動更新,所以如果想將屬性設置爲私有的,那麼getter方法一定要返回相應的類型,即:
public ObservableInt getAge() {
return age;
}
4.3 Observable Collections
Data binding提供了具有通知功能的集合類,如ObservableArrayMap
,ObservableArrayList
,
ObservableArrayMap
繼承自ArrayMap
,並實現了ObservableMap
接口,使用方式和Map
一樣,只是內部實現具有自動的通知機制。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
<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"/>
注意:variable的屬性不能包含
<
符號,要用<
代替。
ObservableArrayList
繼承自ArrayList
,並實現了ObservableList
接口,使用方式和List
一樣,只是內部實現具有自動的通知機制。
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<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"/>
5 Generated Binding - Binding的生成
生成的Binding對象連接了layout變量及相關視圖,像之前提到的,Binding對象的包及名稱是可以自定義的,所有生成的Binding對象都繼承自ViewDataBinding
5.1 Creating - 創建
創建方式,上邊已經提到過,主要有以下幾種方式:
使用Binding類的靜態方法,有一個參數的版本和多個參數的版本:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果佈局是用不同機制填充的,我們可以單獨與layout進行綁定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有時Binding不能預知,我們可以使用DataBindingUtil
類:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
// 或
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
5.2 Views With IDs - 帶ID的視圖
每個帶有Id的視圖,都會在binding類裏生成一個對應的public final的字段,Binding在View層級上做一次遍歷,取出所有帶ID的視圖,這種機制要比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;
5.3 Variables - 變量
每個variable變量都會在Binding類裏生成get和set方法,例如
<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>
會生成如下方法
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);
5.4 ViewStubs
ViewStub和其他View類略有不同,它開始不可見,且當它可見或被填充時,它會把其他佈局填充進來,把自己替換掉。
因爲,ViewStub本質上在佈局層級裏是不存在的,因此只有在ViewStub.inflate()之後,才能進行數據綁定,我們可以使用ViewStubProxy
進行操作。
ViewStubActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
// 這樣報轉型錯誤?
// final ViewStubProxy viewStubProxy = new ViewStubProxy(binding.viewStub);
// 這樣明顯不對,但竟然能運行起來,結果也是正確的
// final ViewStubProxy viewStubProxy = binding.viewStub;
// 暫時採用這種方式
final ViewStubProxy viewStubProxy = new ViewStubProxy((ViewStub)findViewById(R.id.viewStub));
viewStubProxy.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
InflatedLayoutBinding layoutBinding = (InflatedLayoutBinding)viewStubProxy.getBinding();
// TODO 爲InflatedLayoutBinding設置數據
}
});
...
...
// 需要的時候,填充進來
if(!viewStubProxy.isInflated()){
viewStubProxy.getViewStub().inflate();
}