Data Binding使用指南

說明:本文是按照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方法,參數是variabletype對應的類。

運行程序後,你就會在界面上看到文字Test User。或者,你可以通過以下方式獲取:

ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());


但實測,並沒有什麼卵用。官方文檔沒有描述清楚,這樣明顯是不行的,怎麼跟視圖關聯的?這塊至少要有個`setContentView(R.layout.activity_main)`吧?但是加上了也是不行。

完整的代碼應該是這樣的:

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&lt;String>"/>
    <variable name="sparse" type="SparseArray&lt;String>"/>
    <variable name="map" type="Map&lt;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"]}'


也可以屬性值用雙引號包裹,表達式內使用&quot;或者反單引號(`)

android:text="@{map[`firstName`}"
android:text="@{map[&quot;firstName&quot;]}"


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,以及它的弟兄們- ObservableBooleanObservableByteObservableChar,ObservableShortObservableIntObservableLongObservableFloatObservableDouble,ObservableParcelableObservableField自己保存只有一個屬性的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提供了具有通知功能的集合類,如ObservableArrayMapObservableArrayList

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&lt;String, Object>"/>
</data><TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

注意:variable的屬性不能包含<符號,要用&lt;代替。

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&lt;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();
}
發佈了19 篇原創文章 · 獲贊 40 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章