Data Binding 數(shù)據(jù)綁定(二)

本文建立在有一定使用 DataBinding 經(jīng)驗的基礎之上,若還不熟悉 DataBinding 的用法,請參考前一篇博客Data Binding 數(shù)據(jù)綁定(一)

在學習 DataBinding 的過程中,參考 Google 官方的 DataBinding 示例 Demo,自己寫了一個 DataBindingPractice Demo,用于練手。整個工程采用 MVP 架構 + DataBinding,歡迎 star、fork 和溝通交流。

本文介紹了 DataBinding 一些稍微高級的用法,主要包括以下四部分內容:

  1. DataBinding 中的數(shù)據(jù)對象(Data Objects)
  2. DataBinding 中生成綁定類(Generated Binding)
  3. DataBinding 中的屬性設置(Attribute Setters)
  4. DataBinding 中的轉換器(Converters)

數(shù)據(jù)對象(Data Objects)

  1. 任何普通的 Java 對象(POJO)都可以被 DataBinding 所使用,但是改變 POJO 對象的屬性值并不會更新 UI 界面的顯示。DataBinding 真正強大之處在于,它可以讓你的數(shù)據(jù)對象具有通知 UI 界面對象的屬性已經(jīng)發(fā)生改變的能力。

  2. 有三種不同的數(shù)據(jù)變化通知機制:

  • Observable objects
  • observable fields
  • observable collection
  1. 如果這其中的一種數(shù)據(jù)對象被綁定到 UI 界面上,當數(shù)據(jù)對象的屬性值發(fā)生變化時,UI 界面會自動更新。

可觀察對象(Observable Objects)

  1. 一個類如果實現(xiàn)了 Observable 接口,那么 DataBinding 則會將一個 listener 綁定到該類上,就可以監(jiān)聽該類對象中的屬性的變化。Observable 接口具有添加和移除 listener 的機制,但是否通知則取決于開發(fā)者。
  2. 為了使開發(fā)更容易,DataBinding 提供了一個名為 BaseObservable 的基類,它用于實現(xiàn) listener 注冊機制。
  3. 實現(xiàn) Observable 的類負責什么時候通知該類的屬性發(fā)生了變化,只需要在類的 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 注解標記過的 getter 方法會在 BR class 文件中生成一個入口,BR class 文件是在 Module 的包下,BR.classR.class 的功能類似。

可觀察屬性(ObservableFields)

  1. 創(chuàng)建一個 Observable 類還是需要一些工作量的,如果開發(fā)者不想花費太多的時間和精力,或者沒有太多的屬性需要觀察監(jiān)聽的話,那么可以使用 ObservableField,或者它的子類: ObservableBooleanObservableByteObservableCharObservableShortObservableIntObservableLongObservableFloatObservableDoubleObservableParcelable
  2. ObservableField 是包含 Observable Object 對象的單一字段。原始版本避免了在獲取過程中做打包和解包的操作。在數(shù)據(jù)對象中使用 ObservableField,需要創(chuàng)建一個 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();
}

可以通過 set 方法和 get 方法存取數(shù)據(jù)

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

可觀察集合(Observable Collections)

  1. 一些應用會使用動態(tài)的結構持有數(shù)據(jù),可觀察容器類允許使用鍵值對的形式來存取數(shù)據(jù)。
  2. 當鍵值對中的鍵是應用型數(shù)據(jù)(比如:String)時,ObservableArrayMap 是非常有用的。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在布局文件中,也可以通過使用 String 類型的鍵來獲取到相應的值。

<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"/>
  1. 當鍵值對中的鍵是 Integer 型的,ObservableArrayList 則是非常有用的。
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);

在布局文件中,也可以通過使用 Integer 類型的鍵來獲取到相應的值。

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


## 生成綁定類(Generated Binding)
1. 生成的綁定類通過布局文件中的 Views 和布局文件中的變量聯(lián)系起來。
2. 如之前所討論的那樣,綁定類的名稱和所在的位置都是可以自定義的。
3. 生成的所有的綁定類都是 `ViewDataBinding` 的子類。

### 構建(Creating)
1. 綁定類在 View Inflate 之后立即被創(chuàng)建,以確保在布局中的表達式被綁定到視圖之前,View 的層次結構不會被打亂。
2. 有幾種方式綁定布局文件,最常用的是使用 Binding 類中的靜態(tài)方法來綁定類。`inflate` 方法調用一次就可以 Inflate View 并將 View 綁定到 Binding 類上。
3. 還有一個更加簡單的方法,只需要一個 `LayoutInflater` 對象和一個 `viewGroup` 對象。
``` Java
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
  1. 如果布局使用另外不同的機制來 inflate,則可以單獨綁定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
  1. 有時候,Binding 類的名字不得而知,在這種情況下,則可以使用 DataBindingUtil 生成該 Binding 類:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

帶 ID 的 View(Views with IDs)

  1. 使用 DataBinding 庫的布局文件,其中的每個帶 ID 的 View,編譯以后,都會在該布局文件對應的 Binding 類中生成一個被 public final 修飾的屬性,Data Binding 會做一個簡單的賦值,在 Binding 類中保存對應 ID 的 View。
  2. 通過這種機制獲取控件比通過 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;

如果使用 DataBinding 庫的話,在布局文件中為控件設置 Id 不是必須的,但是在某些情況下,在代碼中通過 Id 得到控件還是有必要的。

變量(Variables)

布局文件中的每個變量都會生成對應的存取方法,如:

<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 類中,都會生成對應的存取方法,如下所示:

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

ViewStubs

  1. ViewStub 和普通的 View 相比是不一樣的。它們最開始是不可見的,當它們被設置為可見的或者調用 inflate 方法時,ViewStub 會被替換為另外一個控件或布局。
  2. 因為最開始的時候,ViewStub 在布局層級中不可見,Binding 類中對應的控件也應該被移除,以便回收。
  3. 因為在 Binding 類中,所有 View 對應的屬性都是被 final 字段修飾的,所以一個 ViewStubProxy 對象代替該 ViewStub,當 ViewStub 被設置為可見的或調用 inflate 方法之后,開發(fā)者可以通過此代理類 ViewStubProxy 得到對應的 ViewStub。
  4. inflate 一個新的布局時,必須為新的布局創(chuàng)建新的 Binding 類。所以 ViewStubProxy 必須監(jiān)聽 ViewStub 的 ViewStub.OnInflateListener,當 ViewStub 被 inflate 的時候,則建立一個新的 Binding 類。
  5. 因為 ViewStub 只能設置一個 OnInflateListener,開發(fā)者可以為 ViewStubProxy 設置一個 OnInflateListener,在 Binding 類被建立以后,OnInflateListener 就會被觸發(fā)。
    代碼如下所示:
<layout>
  ...
  <ViewStub
      android:id="@+id/viewStub"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout="@layout/layout_view_stub"/>
  ...
</layout>
mBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub stub, View inflated) {
                LayoutViewStubBinding mStubBinding = DataBindingUtil.findBinding(inflated);
                mStubBinding.tvViewStub.setOnClickListener((view1 -> showClickToast()));
            }
          });

高級綁定(Advanced Binding)

動態(tài)變量(Dynamic Variables)

  1. 有時候,一些 Binding 類不為人所知。比如,在 RecyclerView.Adapter 中可以用來處理不同的布局,此時便不知道該 Binding 類具體是什么類型的。而在 onBindViewHolder(VH, int) 方法中,ViewHolder 中的 Binding 類又必須被賦值。
  2. 在這個例子中,所有 RecyclerView 涉及到的布局中,都有一個 item 的變量。
  3. Adapter 所使用的 ViewHolder 中有一個 getBinding 的方法得到一個 ViewDataBinding 的 Binding 類。如下所示:
public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

立即綁定(Immediate Binding)

當變量或者 observable 變量發(fā)生變化時,會在下一幀才觸發(fā) Binding,但是有時候需要立即 Binding,可以通過 executePendingBindings() 方法立即觸發(fā) Binding。

后臺線程

只要不是集合類型的數(shù)據(jù),你可以在后臺線程中更改數(shù)據(jù)。Data Binding 會在計算時將每個變量/字段在各個線程中做一份數(shù)據(jù)拷貝,以避免同步問題。

屬性設置(Attribute Setters)

當一個屬性值發(fā)生變化時,生成的 Binding 類必須調用該控件對應 data binding 表達式的 setter 方法。Data Binding 框架允許自定義調用何種方法改變值。

自動設置屬性(Automatic Setters)

  1. 對于一個屬性 attribute,Data Binding 會嘗試著去找 setAttribute 方法。屬性的命名空間是什么并沒有什么關系,只和屬性本身的名稱有關。例如,為 TextView 的屬性 android:text 設置了一個 binding 表達式,則 Data Binding 庫會去尋找 setText(String) 的方法。
  2. 如果 data binding 表達式返回了一個 int 型數(shù)據(jù),Data Binding 則會去尋找 setText(int) 的方法。對于 data binding 表達式的返回值一定要小心處理,如果必要的話,需要做類型強制裝換。
  3. 需要注意的是,就算給定名稱的屬性不存在,Data Binding也會生效。正是因為如此,使用 Data Binding 則可以方便地自定義屬性。例如,DrawerLayout 控件并沒有什么屬性,但是卻有很多的 setters 方法,就可以方便地使用自動設置屬性給 DrawerLayout 設置屬性。
<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

重命名屬性設置(Renamed Setters)

有些屬性有其對應的 setter 方法,但是該 setter 方法和其屬性名稱并不是那么相匹配。對于這些方法,可以使用 BindingMethods 注解將該屬性與對應的方法關聯(lián)起來。例如:屬性 android:tint 真正是和 setImageTintList(ColorStateList) 關聯(lián)起來的,而不是和 setTint 方法關聯(lián):

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

在 Android 框架中實現(xiàn)的屬性的 setter 方法已經(jīng)不錯,所以不需要開發(fā)者重命名屬性設置了。

自定義屬性設置(Custom Setters)

  1. 一些屬性需要自定義邏輯。例如,沒有一個 setter 方法和屬性 android:paddingLeft 相關聯(lián),但是卻存在 setPadding(left, top, right, bottom) 方法。被 BindingAdapter 注解修飾的靜態(tài) binding adapter 方法允許開發(fā)者自定義一個屬性的 setter 方法如何被調用。
    Android 已經(jīng)內置了一些 BindingAdapters。如下是一個與屬性 paddingLeft 相關聯(lián)的 setter 方法。
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}
  1. Binding adapters 在其他自定義類型上也非常好用。
    當開發(fā)者自定義的 binding adapters 與默認的 adapters 沖突時,開發(fā)者自定義的會覆蓋默認的。
    當然也可以自定義接收多個參數(shù)的 adapters,一個在非主線程中加載圖片的 Loader 如下所示:
@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);
}
<ImageView
  app:imageUrl="@{venue.imageUrl}"
  app:error="@{@drawable/venueError}"/>
  ```
如果在一個 ImageView 中 `imageUrl` 屬性和 `error` 屬性同時被使用,并且 `imageUrl` 是 String 類型的,`error` 屬性是 Drawable 類型的,則這個 `adapter` 將會被調用。
  * 在匹配的過程中,自定義的命名空間將會被忽略
  * 也可以為 android 命名空間編寫 adapter

3. `binding adapter` 中的方法可以獲取舊值,只需要將舊值放置在前,而新值放置在后,如下所示:
``` Java
@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());
 }
}
  1. 事件處理 handlers 中,只可用于只擁有一個抽象方法的接口或抽象類,如下所示:
@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);
        }
    }
}
  1. 當一個 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);
}

因為改變一個 listener 必將會影響到另一個,所以我們必須有三個不同binding adapters,包括修改一個屬性和修改兩個屬性的,如下所示:

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

上面這個例子比正常情況下要更復雜一些,因為 View 是通過在代碼使用 add/remove 方法添加和移除 View.OnAttachStateChangeListener,而不是通過 setter 方法設置監(jiān)聽器的。android.databinding.adapters.ListenerUtil 可以用來跟蹤之前的 listener,并可以在 Binding Adaper 中移除監(jiān)聽器 listener
通過向 OnViewDetachedFromWindowOnViewAttachedToWindow 接口添加 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 注解,Data Binding 代碼生成器知道監(jiān)聽器只在 Honeycomb MR1 設備或更新版本的設備中使用。

轉換器(Converters)

對象轉換(Object Conversions)

當 binding 表達式返回一個對象時,一個 setter 方法(自動 Setter,重命名 Setter,自定義 Setter),并將返回的對象強制轉換成所選擇的 setter 方法所需要的類型。
以下是一個使用 ObservableMaps 持有數(shù)據(jù)并轉換的例子:

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

userMap 返回一個對象,并且這個對象會被自動地轉換為 setter setText(CharSequence) 所需要的類型。當參數(shù)類型選擇存在疑惑時,需要開發(fā)者手動地將數(shù)據(jù)類型進行轉換。

自定義類型轉換器(Custom Conversions)

有時候,屬性的值需要在特定類型之間自動轉換。例如,在設置背景的時候:

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

在這里,背景需要 Drawable 類型的,但是顏色卻是 Integer 類型的。當需要一個 Drawable,binding 表達式返回的卻是 Integer 的,所以此 int 型數(shù)據(jù)應該轉換成 ColorDrawable,此轉換可以通過一個被 BindingConversion 注解修飾的靜態(tài)方法完成。

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

需要注意的是,此轉換只能在 setter 階段完成,所以它不允許如下面這樣混合類型的:

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

DataBinding 第二篇文章也介紹完成。至此,關于 DataBinding 的文章到此就告一段落。如果有什么問題歡迎指出。我的工作郵箱:jiankunli24@gmail.com


參考資料:

DataBInding 官方文檔

深入Android Data Binding(一):使用詳解 -- YamLee

Android Data Binding 系列(一) -- 詳細介紹與使用 -- ConnorLin

DataBinding(一)-初識 -- sakasa

(譯)Data Binding 指南 -- 楊輝

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容