(譯)Data Binding 指南

anysort 8年前發布 | 11K 次閱讀 安卓開發 Android開發 移動開發

來自: http://yanghui.name/blog/2016/02/17/data-binding-guide/

翻譯自 http://developer.android.com/intl/zh-cn/tools/data-binding/guide.html

這個文檔用于解釋如何使用 Data Binding Library 編寫聲明式的布局,減少應用中邏輯以及布局所需要的“膠水代碼”。

Data Binding Library 提供了靈活性與通用性 - 它是一個 support library,可以在 Android 2.1(API level 7+)以上的平臺使用。

為了使用 data binding,gradle plugin的版本必須是 1.5.0-alpha1以上。

編譯環境

為了使用 Data Binding,首先在 Android SDK manager 中下載最新的 Support repository。

然后在 build.gradle 中添加 dataBinding 段。

使用以下代碼段配置 data binding:

android {
    ....
    dataBinding {
        enabled = true
    }
}

如果你的 app module 依賴了一個使用 data binding 的庫,那么你的 app module 的 build.gradle 也必須配置 data binding。

同時,確定使用了支持該特性的 Android Studio。在 Android Studio 1.3 以及之后的版本提供了 data binding 的支持,詳見 Android Studio Support for Data Binding

Data Binding 布局文件

編寫你的第一個 data binding 表達式

Data binding 布局文件與普通布局文件有一點不同。它以一個 layout 標簽作為根節點,里面是 data 標簽與 view 標簽。view 標簽的內容就是不使用 data binding 時的普通布局文件內容。以下是一個例子:

<?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"/>

布局文件中的表達式使用 “@{}” 的語法。在這里,TextView 的文本被設置為 user中的 firstName 屬性。

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>

數據對象

假設你有一個 plain-old Java object(POJO) 的 User 對象。

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}

這種類型的對象擁有不可改變的數據。在應用中,讀一次且不變動數據的對象非常常見。也可以使用 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;
   }
}

從 data binding 的角度看,這兩個類是一樣的。用于 TextView 的 android:text 屬性的表達式 @{user.firstName} ,會讀取 POJO 對象的 firstName 域以及 JavaBeans 對象的 getFirstName() 方法。此外,如果 firstName() 方法存在的話也同樣可用。

綁定數據

在默認情況下,會基于布局文件生成一個 Binding 類,將它轉換成帕斯卡命名并在名字后面接上”Binding”。上面的那個布局文件叫 main_activity.xml ,所以會生成一個 MainActivityBinding 類。這個類包含了布局文件中所有的綁定關系( user 變量),會根據綁定表達式給布局文件賦值。在 inflate 的時候創建 binding 的方法如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
}

就這么簡單!運行應用,你會發現測試用戶已經顯示在界面中了。你也可以通過以下這種方式:

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

如果你在 ListView 或者 RecyclerView 的 adapter 中使用 data binding,你可以這樣寫:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

綁定事件

事件可以直接與 handler 函數綁定,類似于 android:onClick 可以指定 Activity 中的一個函數一樣。事件屬性的命名由 listener 的函數命名決定。舉個例子, View.OnLongClickListener 中有一個 onLongClick() 函數,所以這個事件的對應屬性就是 android:onLongClick 。

為了將事件分配給 handler,只需要使用一個 binding 表達式,值為要調用的函數名。舉個例子,如果你的數據對象有兩個函數:

public class MyHandlers {
    public void onClickFriend(View view) { ... }
    public void onClickEnemy(View view) { ... }
}

分配點擊事件的 binding 表達式如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.Handlers"/>
       <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="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
           android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
   </LinearLayout>
</layout>

也有一個特殊的點擊事件 handler,他們有一些不同于 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 標簽內可以有多個 import 標簽。你可以在布局文件中像使用 Java 一樣導入引用。

<data>
    <import type="android.view.View"/>
</data>

現在 View 可以被這樣引用:

<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"/>

現在, Vista 可以用來引用 com.example.real.estate.View ,與 View 在布局文件中同時使用。導入的類型也可以用于變量的類型引用和表達式中:

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List<User>"/>
</data>

注意:Android Studio 還沒有對導入提供自動補全的支持。你的應用還是可以被正常編譯,要解決這個問題,你可以在變量定義中使用完整的包名。

</div>

<TextView
   android:text="@{((User)(user.connection)).lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

導入也可以用于在表達式中使用靜態域/方法:

<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.* 會被自動導入。

變量

data 標簽中可以有任意數量的 variable 標簽。每個 variable 標簽描述了會在 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>

變量類型會在編譯時被檢查,所以如果變量聲明了 Observable 接口或者是一個 可觀察容器類 ,那它會被反射使用。如果變量是一個沒有聲明 Observable* 接口的基類或借口,變量的變動則不會引起 UI 的變化!

當針對不同配置編寫不同的布局文件時(比如橫屏豎屏的布局),變量會被合并。所以這些不同配置的布局文件之間不能存在沖突。

自動生成的 binding 類會為每一個變量生產 getter/setter 函數。這些變量會使用 Java 的默認賦值,直到 setter 函數被調用。默認賦值有 null , 0 ( int ), false ( boolean )等。

binding 類也會生一個一個命名為 context 的特殊變量,這個變量被用于表達式中。 context 變量其實就是 rootView 的 getContext() ) 的返回值。 context 變量會被同名的顯式變量覆蓋。

自定義 Binding 類名

默認情況下,binding 類的名稱取決于布局文件的命名,以大寫字母開頭,移除下劃線,后續字母大寫并追加 “Binding” 結尾。這個類會被放置在 databinding 包中。舉個例子,布局文件 contact_item.xml 會生成 ContactItemBinding 類。如果 module 包名為 com.example.my.app ,binding 類會被放在 com.example.my.app.databinding 中。

通過修改 data 標簽中的 class 屬性,可以修改 Binding 類的命名與位置。舉個例子:

<data class="ContactItem">
    ...
</data>

以上會在 databinding 包中生成名為 ContactItem 的binding 類。如果需要放置在不同的包下,可以在前面加 “.”:

<data class=".ContactItem">
    ...
</data>

這樣的話, ContactItem 會直接生成在 module 包下。如果提供完整的包名,binding 類可以放置在任何包名中:

<data class="com.example.ContactItem">
    ...
</data>

Includes

在使用應用命名空間的布局中,變量可以傳遞到任何 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>

需要注意, 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>

表達式語言

通用特性

表達式語言與 Java 表達式有很多相似之處。下面是相同之處:

  • 數學計算 + - / * %
  • 字符串連接 +
  • 邏輯 && ||
  • 二進制 & | ^
  • 一元 + - ! ~
  • 位移 >> >>> <<
  • 比較 == > < >= <=
  • instanceof
  • 組 ()
  • 字面量 - 字符,字符串,數字, null
  • 類型轉換
  • 函數調用
  • 域存取
  • 數組存取 []
  • 三元運算符 ?:

例子:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

缺失的操作符

一些 Java 中的操作符在表達式語法中不能使用。

  • this
  • super
  • new
  • 顯式泛型調用 <T>

Null合并運算符

Null合并運算符( ?? )會在非 null 的時候選擇左邊的操作,反之選擇右邊。

android:text="@{user.displayName ?? user.lastName}"

等同于

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

屬性引用

首先是先前 編寫你的第一個 data binding 表達式 中所提到的:JavaBean 引用。當表達式引用了一個類內的屬性時,他會嘗試直接調用域,getter,還有 ObservableFields。

android:text="@{user.lastName}"

避免NullPointerException

自動生成的 data binding 代碼會自動檢查和避免 null pointer exceptions。舉個例子,在表達式 @{user.name} 中,如果 user 是 null, user.name 會賦予默認值 null 。如果你引用了 user.age ,因為 age 是 int 類型,所以默認賦值為 0。

容器類

通用的容器類:數組,lists,sparse lists,和 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]}"

字符串字面量

使用單引號把屬性包起來,就可以很簡單地在表達式中使用雙引號:

android:text='@{map["firstName"]}'

也可以用雙引號將屬性包起來。這樣的話,字符串字面量就可以用 &quot; 或者反引號(`) 來調用

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

資源

也可以在表達式中使用普通的語法來引用資源:

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)}"</pre>

一些資源需要顯示類型調用。

Class Listener Setter Attribute
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

數據對象

任何 POJO 都能用在 data binding 中,但是更改 POJO 并不會同步更新 UI。data binding 的強大之處就在于它可以讓你的數據擁有更新通知的能力。這里有三種不同的數據變動通知機制, Observable 對象observable 域 ,與 observable 容器類

當以上的 observable 對象綁定在 UI 上,數據發生變化時,UI 就會同步更新。

Observable 對象

當一個類聲明了 Observable 接口時,data binding 會設置一個 listener 在綁定的對象上,以便監聽對象域的變動。

Observable 接口有一個添加/移除 listener 的機制,但通知取決于開發者。為了簡化開發,我們創建了一個基類 BaseObservable ,來實現 listener 注冊機制。這個類也實現了域變動的通知,你只需要在 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 注解會在編譯時在 BR 類內生成一個元素。而 BR 類會生成在 module 的 package 下。如果數據基類不可修改, Observable 接口的存儲和 listener 通知可以用 PropertyChangeRegistry 來實現。

Observable域

創建 Observable 類還是需要花費一點時間的,如果開發者想要省時,或者數據類的域很少的話,可以使用 ObservableField 以及它的派生 ObservableBoolean ObservableByte ObservableChar ObservableShort ObservableInt ObservableLong ObservableFloat ObservableDouble ObservableParcelable 。 ObservableFields 是單一域的自包含 observable 對象。原始版本避免了在存取過程中做打包/解包操作。要使用它,在數據類中創建一個 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();
}

就這么簡單!要存取數據,只需要使用 get set 方法:

user.firstName.set("Google");
int age = user.age.get();

Observable 容器類

一些應用會使用更加靈活的結構來保持數據。Observable 容器類允許使用 key 來獲取這類數據。當 key 是類似 String 的一類引用類型時,使用 ObservableArrayMap 會非常方便。

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在布局中,可以用 String key 來獲取 map 中的數據:

<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"/>

當 key 是整數類型時,可以使用 ObservableArrayList

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"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

生成Binding

生成的 binding 類將布局中的 View 與變量綁定在一起。就像先前提到過的,類名和包名可以 自定義 。生成的 binding 類會繼承 ViewDataBinding

創建

binding 應該在 inflate 之后創建,確保 View 的層次結構不會在綁定前被干擾。綁定布局有好幾種方式。最常見的是使用 binding 類中的靜態方法。inflate 函數會 inflate View 并將 View 綁定到 binding 類上。此外有更加簡單的函數,只需要一個 LayoutInflater 或一個 ViewGroup:

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);

如果布局使用不同的機制來 inflate,則可以獨立做綁定操作:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有時綁定關系是不能提前確定的。這種情況下,可以使用 DataBindingUtil

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

帶有 ID 的 View

布局中每一個帶有 ID 的 View,都會生成一個 public final 域。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;

ID 在 data binding 中并不是必需的,但是在某些情況下還有有必要對 View 進行操作。

變量

每一個變量會有相應的存取函數:

<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 類中生成對應的 getter setter:

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);

ViewStub

ViewStub 相比普通 View 有一些不同。ViewStub 一開始是不可見的,當它們被設置為可見,或者調用 inflate 方法時,ViewStub 會被替換成另外一個布局。

因為 ViewStub 實際上不存在于 View 結構中,binding 類中的類也得移除掉,以便系統回收。因為 binding 類中的 View 都是 final 的,所以我們使用了一個叫 ViewStubProxy 的類來代替 ViewStub 。開發者可以使用它來操作 ViewStub,獲取 ViewStub inflate 時得到的視圖。

但 inflate 一個新的布局時,必須為新的布局創建一個 binding。因此, ViewStubProxy 必須監聽 ViewStub 的 ViewStub.OnInflateListener ,并及時建立 binding。由于 ViewStub 只能有一個 OnInflateListener,你可以將你自己的 listener 設置在 ViewStubProxy 上,在 binding 建立之后, listener 就會被觸發。

高級 binding

動態變量

有時候,有一些不可知的 binding 類。例如, RecyclerView.Adapter 可以用來處理不同布局,這樣的話它就不知道應該使用哪一個 binding 類。而在 onBindViewHolder(VH, int) ) 的時候,binding 類必須被賦值。

在這種情況下,RecyclerView 的布局內置了一個 item 變量。 BindingHolder 有一個 getBinding 方法,返回一個 ViewDataBinding 基類。

public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

直接 binding

當變量或者 observable 發生變動時,會在下一幀觸發 binding。有時候 binding 需要馬上執行,這時候可以使用 executePendingBindings() )。

后臺線程

只要數據不是容器類,你可以直接在后臺線程做數據變動。Data binding 會將變量/域轉為局部量,避免同步問題。

屬性 Setter

當綁定數據發生變動時,生成的 binding 類必須根據 binding 表達式調用 View 的 setter 函數。Data binding 框架內置了幾種自定義賦值的方法。

自動 Setter

對一個 attribute 來說,data binding 會嘗試尋找對應的 setAttribute 函數。屬性的命名空間不會對這個過程產生影響,只有屬性的命名才是決定因素。

舉個例子,針對一個與 TextView 的 android:text 綁定的表達式,data binding會自動尋找 setText(String) 函數。如果表達式返回值為 int 類型, data binding則會尋找 setText(int) 函數。所以需要小心處理函數的返回值類型,必要的時候使用強制類型轉換。需要注意的是,data binding 在對應名稱的屬性不存在的時候也能繼續工作。你可以輕而易舉地使用 data binding 為任何 setter “創建” 屬性。舉個例子,support 庫中的 DrawerLayout 并沒有任何屬性,但是有很多 setter,所以你可以使用自動 setter 的特性來調用這些函數。

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

重命名 Setter

一些屬性的命名與 setter 不對應。針對這些函數,可以用 BindingMethods 注解來將屬性與 setter 綁定在一起。舉個例子, android:tint 屬性可以這樣與 setImageTintList(ColorStateList) ) 綁定,而不是 setTint :

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

Android 框架中的 setter 重命名已經在庫中實現了,開發者只需要專注于自己的 setter。

自定義 Setter

一些屬性需要自定義 setter 邏輯。例如,目前沒有與 android:paddingLeft 對應的 setter,只有一個 setPadding(left, top, right, bottom) 函數。結合靜態 binding adapter 函數與 BindingAdapter 注解可以讓開發者自定義屬性 setter。

Android 屬性已經內置一些 BindingAdapter。例如,這是一個 paddingLeft 的自定義 setter:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

Binding adapter 在其他自定義類型上也很好用。舉個例子,一個 loader 可以在非主線程加載圖片。

當存在沖突時,開發者創建的 binding adapter 會覆蓋 data binding 的默認 adapter。

你也可以創建多個參數的 adapter:

@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); }

</div>

<ImageView app:imageUrl=“@{venue.imageUrl}” app:error=“@{@drawable/venueError}”/>

</div>

imageUrlerror 存在時這個 adapter 會被調用。imageUrl 是一個 String,error 是一個 Drawable。

  • 在匹配時自定義命名空間會被忽略
  • 你可以為 android 命名空間編寫 adapter

Binding adapter 方法可以獲取舊的賦值。只需要將舊值放置在前,新值放置在后:

@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());
   }
}

事件 handler 僅可用于只擁有一個抽象方法的接口或者抽象類。例如:

@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); }</pre>

因為改變一個 listener 會影響到另外一個,我們必須編寫三個不同的 adapter,包括修改一個屬性的,和修改兩個屬性的。

@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);
    }
}

}</pre>

上面的例子比普通情況下復雜,因為 View 是 add/remove View.OnAttachStateChangeListener 而不是 set。 android.databinding.adapters.ListenerUtil 可以用來輔助跟蹤舊的 listener 并移除它。

對應 addOnAttachStateChangeListener(View.OnAttachStateChangeListener) ) 支持的 api 版本,通過向 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1) 注解,data binding 代碼生成器會知道這些 listener 只會在 Honeycomb MR1 或更新的設備上使用。

轉換器

對象轉換

當 binding 表達式返回對象時,會選擇一個 setter(自動 Setter,重命名 Setter,自定義 Setter),將返回對象強制轉換成 setter 需要的類型。

下面是一個使用 ObservableMap 保存數據的例子:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

在這里, userMap 會返回 Object 類型的值,而返回值會被自動轉換成 setText(CharSequence) 所需要的類型。當對參數類型存在疑惑時,開發者需要手動做類型轉換。

自定義轉換

有時候會自動在特定類型直接做類型轉換。例如,當設置背景的時候:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

在這里,背景需要的是 Drawable ,但是 color 是一個整數。當需要 Drawable 卻返回了一個整數時, int 會自動轉換成 ColorDrawable 。這個轉換是在一個 BindingConversation 注解的靜態函數中實現:

<p>@BindingConversion public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); }</p>

需要注意的是,這個轉換只能在 setter 階段生效,所以 不允許 混合類型:

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Android Studio 對 Data binding 的支持

Android Studio 支持 data binding 表達式的高亮,并會在編輯器中標出表達式中的語法錯誤。

在預覽窗口顯示的是 data binding 表達式的默認值。下面是一個設置默認值的例子, TextView 的 text 默認值為 PLACEHOLDER 。

<TextView android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{user.firstName, default=PLACEHOLDER}"/>

如果你需要在設計階段顯示默認值,你可以使用 tools 屬性代替默認值表達式,詳見 設計階段布局屬性

</div>

 本文由用戶 anysort 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!