Data Objects
POJO(plain old Java object) 可以用來作為 data binding
但是 POJO 的變動不會導致 UI 的更新
如果要真正發揮 data binding 的能力可以使用以下三種通知機制之一
Observable objects, observable fields, observable collections
Observable Objects
實作 Observable interface 的類別允許 binding 附加一個 listener 來監聽這個物件的屬性改變
Observable interface 有 add/remove 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; //加了 Bindable 標記會在編譯時在 module package 下生成一個 BR class notifyPropertyChanged(BR.firstName); } public void setLastName(String lastName) { this.lastName = lastName; notifyPropertyChanged(BR.lastName); } }
如果 data classes 的 base class 無法是 BaseObservable
可以實作 Observable interface 利用 PropertyChangeRegistry 去達成目的
Observable fields
如果覺得建立 Observable classes 的方式有點麻煩
我們也可以使用 ObservableField 與其兄弟姊妹
ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, ObservableParcelable
ObservableField 是內含單一屬性欄位的 observable object
而 primitive 的版本則是在存取時避免了 boxing 跟 unboxing 動作
如果要使用 Observable fields 記得要將其宣告為 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 method
user.firstName.set("Google"); int age = user.age.get();
Observable collections
動態資料結構的話
Observable collections 允許使用 key 來存取資料
如果 key 是 reference type 的話
可以用 ObservableArrayMap
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>(); user.put("firstName", "Google"); user.put("lastName", "Inc."); user.put("age", 17);
在 layout 檔中的使用方法也是透過 key 來存取
<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 是 integer 的話
就使用 ObservableArrayList
ObservableArrayList<Object> user = new ObservableArrayList<>(); user.add("Google"); user.add("Inc."); user.add(17);在 layout 檔中是透過索引來存取
<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
自動產生的 binding 類別負責連接 layout variables 跟 Views
(如前篇所述,其名稱跟 package 可以自訂)
所有生成的 binding class 都繼承自 ViewDataBinding
Creating
binding 應在 inflate 後馬上建立以確保 View hierarchy 裡的 view 的 binding 及 layout 裡面的 expressions 能有效運作
而 binding 的方式不只一種
較常見的是使用 Binding class 的 static methods 將 inflate 跟 binding 一步完成
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater); MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);若是 layout 已經 inflate 出來了
那就只要進行 binding 就好
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
所以前篇範例中原本的
ActivityMainBinding testbinding = DataBindingUtil.setContentView(this, R.layout.activity_main);由於 binding 的部分都在 include 進來的 content_main.xml 中(其 id 我們取為 contentMain)
可以改為
setContentView(R.layout.activity_main); ContentMainBinding testbinding = ContentMainBinding.bind(findViewById(R.id.contentMain));把 setContentView 跟 bind 分開
有的時候我們無法提前知道要用哪個 binding 類別
這時可以用 DataBindingUtil class
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent); ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
Views With IDs and Variables
layout 裡的每個 view 都會照它們的 id 生成一個 public final 欄位(沒有給 id 的就不會有)
這會比我們自己一個個去 findViewById 更快
(也就是說原則上不需要 data binding 的 view 就不用給 id,但當然這並非絕對)
而每個變數欄位都會生成 setter 跟 getter
ViewStubs
ViewStubs 跟正常的 view 有些不同
它們一開始是不可見的,直到被設為 visible 或是被 inflate 的時候,它們才會被 inflate 出來
因為原本在 View hierarchy 中不存在,所以 binding object 的 view 當然也不存在
而 Views 是 final 的,所以會先用 ViewStubProxy 代替 ViewStub
以便在 inflate 之後能操控 ViewStub
在 inflate 一個 layout 以後,新的 layout 必須做 binding 的動作
因此 ViewStubProxy 必須監聽 ViewStub.OnInflateListener 並在這時建立 binding
又 ViewStub 跟 ViewStubProxy 不會同時存在
所以 ViewStubProxy 也允許設定 OnInflateListener (在 binding 建立後會被呼叫)
Advanced Binding
Dynamic Variables
有時我們無法肯定哪個 binding 要被用
例如 RecyclerView.Adapter 可能不會只使用某一特定 layout
但卻必須把 value 在 onBindViewHolder(VH, int) 給綁定
這時我們可以透過 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(); }
Immediate Binding
當變數或 observable 改變時
binding 會排程在下一個 frame 前更新
若要強制馬上執行可以用 executePendingBindings()
Background Thread
若資料不是 collection 型態,可以在 background thread 改變資料
data binding 會驗證每個欄位以避免錯誤並更新數值
Attribute Setters
當綁定的資料改變時,binding class 會呼叫 setter
由於 data binding framework 會自動挑選最適合的 setter 來設值
例如 android:text 一般是用 setText(String)
但如果 expression 回傳的值是 int,則會尋找是否有 setText(int)
因此若回傳值不是預期的型別,記得要先轉型
不過即使沒有符合名稱的屬性,data binding 還是會運作
如下例 app:scrimColor 跟 app:drawerListener 都是自訂的屬性
<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 跟屬性的名稱並不相符
這時可以加上 BindingMethods 標記
例如 android:tint 其實是對應到 setImageTintList(ColorStateList)
開發者不應該要處理這種事情而應該由 android framework 來實作
@BindingMethods({ @BindingMethod(type = "android.widget.ImageView", attribute = "android:tint", method = "setImageTintList"), })
Custom Setters
有些屬性需要自訂 binding 邏輯
例如沒有 setter 是對應到 android:paddingLeft 而是 setPadding(left, top, right, bottom)
我們可以用加了 BindingAdapter 標記的 static method 來自定 setter
@BindingAdapter("android:paddingLeft") public static void setPaddingLeft(View view, int padding) { view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); }
BindingAdapter 也可以用來指定自訂屬性的對應 setter
@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}”/>如上例 loadImage 在設定 imageUrl 跟 error 屬性時都會被呼叫
且 imageUrl 是一個 string 而 error 是一個 drawable
(注意到 namespaces 是被忽略的,所以我們也可以覆蓋 android 這個 namespace 裡的屬性 setter)
Binding adapter method 還可以將原本的舊值取來用
(不過這不是必要的參數)
記住如果要用的話記得所有的舊值要在新值前面
@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()); } }
Event handler 的部分只限於用 interface 或只有一個 abstract method 的 abstract class
@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 有多個 method
就必須分成不同 listeners
例如 View.OnAttachStateChangeListener 有 onViewAttachedToWindow() 跟 onViewDetachedFromWindow() 兩個 method
所以要先建立兩個 interface 把它們分開
@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 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); } } }android.databinding.adapters.ListenerUtil 類別可以有助於追蹤到原有的 listeners
讓我們可以在 Binding Adaper 裡面將它移除
官方提到這個例子比正常的狀況複雜
因為 View 的 listener 一般是用 add 跟 remove 而不是 setter
由於我們加上 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 標記
data binding code generator 知道這只會在 Honeycomb MR1 上執行
新的系統版本會使用 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)
addOnAttachStateChangeListener(View.OnAttachStateChangeListener)
Converters
前面提到 data binding 會自動挑選適合的 setter
<TextView android:text='@{userMap["lastName"]}' android:layout_width="wrap_content" android:layout_height="wrap_content"/>但如果型別會造成混亂
就必須自訂
<View android:background="@{isError ? @color/red : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>background 要求的是 Drawable 型別
但是拿到的卻是 int
所以我們必須使用 BindingConversion 標記
先把值轉成 ColorDrawable
@BindingConversion public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); }不過不允許混用型別
像下面這樣就是不行的
<View android:background="@{isError ? @drawable/error : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
相關資料:
Data Binding Guide
Presentation - A Brief Intro of Android Data Binding
沒有留言:
張貼留言