Two-way data binding (雙向數據綁定)

前言


使用單向數據綁定可以在屬性上設置數值,並可設定對該屬性的變化作出反應的監聽器。如下
<CheckBox  
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>

雙向數據綁定提供了此過程的捷徑。如下
<CheckBox  
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>

注意在 @={} 表示法中的 = 符號表示了同時接受資料變化以及監聽更新。

為了對數據的變化做出反應,可以讓佈局變量成為 Observable 的實現,通常是 BaseObservable,並使用 @Bindable 註釋,如下
public class LoginViewModel extends BaseObservable {  
// private Model data = ...

@Bindable
public Boolean getRememberMe() {
return data.rememberMe;
}

public void setRememberMe(Boolean value) {
// Avoids infinite loops.
if (data.rememberMe != value) {
data.rememberMe = value;

// React to the change.
saveData();

// Notify observers of a new value.
notifyPropertyChanged(BR.remember_me);
}
}
}

由於 bindable 屬性的 getter 方法稱為 getRememberMe(),因此屬性的相應 setter 方法會自動使用名稱 setRememberMe()。

關於使用 BaseObservable 和 @Bindable 的更多訊息,參考 Work with observable data objects.

Two-way data binding using custom attributes


該平台為最常見的雙向屬性和監聽器提供雙向數據綁定的實現,使用者可以將當作應用程序的一部分。

若使用者想在自定義屬性上使用雙向資料綁定則必須了解如何使用 @InverseBindingAdapter 和 @InverseBindingMethod 註解。

例如,如果要在名稱為 MyView 的自定義視圖中對 "time" 屬性啟用雙向數據綁定,必須完成以下步驟

1.註釋設置初始值的方法,並在使用 @BindingAdapter 更改時進行更新
@BindingAdapter("time")  
public static void setTime(MyView view, Time newValue) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue;
}
}

2.使用 @InverseBindingAdapter 註釋從視圖中讀取數值的方法
@InverseBindingAdapter("time")  
public static Time getTime(MyView view) {
return view.getTime();
}

此時,data binding library 知道數據變化時要執行的操作(呼叫使用 @BindingAdapter 註釋的方法)以及視圖屬性變化時調用的內容(它呼叫 InverseBindingListener)。

為此,需要在視圖上設置一個監聽器。它可以是與自定義視圖關聯的自定義監聽器,也可以是通用事件,例如失去焦點或文本更改。

將 @BindingAdapter 註釋添加到為該屬性的變化設置監聽器的方法,如下
@BindingAdapter("app:timeAttrChanged")  
public static void setListeners(
MyView view, final InverseBindingListener attrChange) {
// Set a listener for click, focus, touch, etc.
}

這個 Listener 包含了 InverseBindingListener 作為參數,可以使用 InverseBindingListener 告知 data binding library 該屬性已更改。接著,系統可以開始使用 @InverseBindingAdapter 呼叫註釋方法,依此類推。

注意每個雙向綁定都會生成一個合成事件屬性( synthetic event attribute)。此屬性與基本屬性具有相同的名稱,但具有後綴 "AttrChanged"。

合成事件屬性允許 data binding library 建立使用 @BindingAdapter 註釋的方法,以將事件監聽器連結到 View 的相應實體。

在實作中,該監聽器包括一些特殊的邏輯,包括用於單向數據綁定的監聽器。相關範例,參考TextViewBindingAdapter

 

Converters


如果綁定到 View 對象的變數需要在顯示之前進行格式化,轉換或更改,則可以使用 Converter 對象。

例如,使用顯示日期的 EditText 對象
<EditText  
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>

viewmodel.birthDate 屬性包含 Long 類型的值,因此需要使用轉換器對其進行格式化。

因為使用雙向表達式,所以還需要一個反向轉換器(inverse converter)讓 data binding library 知道如何將字符串轉換回數據類型,在本例中為 Long。

此過程通過將 @InverseMethod 註釋添加到其中一個轉換器並使此註釋引用反向轉換器來完成。如下
public class Converter {  
@InverseMethod("stringToDate")
public static String dateToString(EditText view, long oldValue,
long value) {
// Converts long to String.
}

public static long stringToDate(EditText view, String oldValue,
String value) {
// Converts String to long.
}
}

 

Infinite loops using two-way data binding


使用雙向數據綁定時,小心不要引入無限迴圈。

當用戶更改屬性時,將呼叫使用 @InverseBindingAdapter 註釋的方法,並將數值分配給 屬性。反過來,這將呼叫使用 @BindingAdapter 註釋的方法,這將觸發對使用 @InverseBindingAdapter 註釋的方法的另一個調用,依此類推。

因此,通過比較使用 @BindingAdapter 註釋的方法中的新値和舊值來打破可能的無限循環非常重要。

 

Two-way attributes


下表為平台預設的雙向綁定屬性表。














Class
Attribute(s)
Binding adapter

AdapterView
android:selectedItemPosition
android:selection

AdapterViewBindingAdapter

CalendarView
android:date
CalendarViewBindingAdapter

CompoundButton
android:checked
CompoundButtonBindingAdapter

DatePicker
android:year
android:month
android:day

DatePickerBindingAdapter

NumberPicker
android:value
NumberPickerBindingAdapter

RadioButton
android:checkedButton
RadioGroupBindingAdapter

RatingBar
android:rating
RatingBarBindingAdapter

SeekBar
android:progress
SeekBarBindingAdapter

TabHost
android:currentTab
TabHostBindingAdapter

TextView
android:text
TextViewBindingAdapter

TimePicker
android:hour
android:minute

TimePickerBindingAdapter




Orignal From: Two-way data binding (雙向數據綁定)

Bind layout views to Architecture Components (綁定佈局到架構元件)

前言


AndroidX 函式庫包含架構元件(Architecture Components),可以使用它來設計健壯,可測試和可維護的應用程序。

Data Binding Library 可與架構元件無縫協作,進一步簡化 UI 的開發。

應用程序中的 layout 可以綁定到架構元件中的數據,這些數據可以幫助管理 UI 控制器生命週期並通知數據中的更改。

以下介紹如何將架構元件合併到應用程序,以進一步使用 Data Binding Library 的好處。

 

Use LiveData to notify the UI about data changes


可以使用 LiveData 物件作為數據綁定來源,以自動通知UI有關數據更改的訊息。更多的資訊可以參考 LiveData Overview

與實現 Observable 的物件(例如 observable fields)不同,LiveData 物件了解訂閱對象的生命週期。相關的好處可以參考 The advantages of using LiveData

在 Android Studio 3.1 或之後的版本可以使用 LiveData 取代 observable fields。

要將 LiveData 與綁定類別一起使用,需要指定生命週期所有者(lifecycle owner)以定義 LiveData 對象的範圍。如下,在綁定類別初始化之後指定了 Activity 本身為生命週期所有者。
class ViewModelActivity extends AppCompatActivity {  
@Override
protected void onCreate(Bundle savedInstanceState) {
// Inflate view and obtain an instance of the binding class.
UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this);
}
}

可以使用 ViewModel元件(如 Use ViewModel to manage UI-related data)將數據綁定到佈局。在 ViewModel 中,可以使用 LiveData 對象轉換數據或合併多個數據來源。

如下示範如何在 ViewModel 中轉換數據。
class ScheduleViewModel extends ViewModel {  
LiveData username;

public ScheduleViewModel() {
String result = Repository.userName;
userName = Transformations.map(result, result -> result.value);
}

 

Use VeiwModel to manage UI-related data


Data binding library 和 ViewModel 可以無縫協作,以顯示數據並對其更改做出反應。

將 ViewModel 元件與 data binding library 一起使用,可以將 UI 邏輯從佈局移動到元件中,讓元件更易於測試。

Data binding library 可確保在需要時綁定和取消綁定數據來源。

更多的訊息可以參考 ViewModel Overview

要將 ViewModel 與 data binding library 一起使用,必須實體化繼承自 ViewModel 的元件,取得綁定類別的實體,並將 ViewModel 元件分配給綁定類別中的屬性。如下
class ViewModelActivity extends AppCompatActivity {  
@Override
protected void onCreate(Bundle savedInstanceState) {
// Obtain the ViewModel component.
UserModel userModel = ViewModelProviders.of(getActivity())
.get(UserModel.class);

// Inflate view and obtain an instance of the binding class.
UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

// Assign the component to a property in the binding class.
binding.viewmodel = userModel;
}
}

在 layout 中,使用表達式將 ViewModel 元件的屬性和方法分配給相對應的視圖。
<CheckBox  
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />

 

Use an Observable ViewModel for more control over binding adapters


可以使用實現 Observable 的 ViewModel 來通知其他應用程序元件有關數據更改的信息,類似於使用 LiveData 對象的方式。

在某些情況下,可能更適合使用 ViewModel 元件來實現 Observable 介面而不是使用 LiveData 對象,即使沒有 LiveData 的生命週期管理功能。

使用實現 Observable 的 ViewModel 元件可以更好地控制應用程序中的綁定適配器。此模式可以在數據變化時更容易控制通知,允許指定自定義方法以在雙向數據綁定中設置屬性的值。

要實現可觀察的 ViewModel 元件,必須創建一個繼承 ViewModel 並實現 Observable 介面的類別。

當觀察者使用 addOnPropertyChangedCallback() 和 removeOnPropertyChangedCallback()方法訂閱或取消訂閱通知時,使用者可以提供自定義邏輯。

還可以提供在 notifyPropertyChanged()方法中屬性更改時執行的自定義邏輯。

以下示範如何實現可觀察的 ViewModel
/**  
* A ViewModel that is also an Observable,
* to be used with the Data Binding Library.
*/
class ObservableViewModel extends ViewModel implements Observable {
private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();

@Override
protected void addOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback) {
callbacks.add(callback);
}

@Override
protected void removeOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback) {
callbacks.remove(callback);
}

/**
* Notifies observers that all properties of this instance have changed.
*/
void notifyChange() {
callbacks.notifyCallbacks(this, 0, null);
}

/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
void notifyPropertyChanged(int fieldId) {
callbacks.notifyCallbacks(this, fieldId, null);
}
}

 

Orignal From: Bind layout views to Architecture Components (綁定佈局到架構元件)

Binding adapters (綁定轉接器)

前言


Binding Adapter 主要用來呼叫適當方式來設定數值。一個例子為透過 setText 方法來設定屬性值,另一個例子為透過呼叫 setOnClickListener 方法來設定事件監聽器。

Data Binding Library 讓使用者指定調用方法來設定數值,客製化綁定邏輯,並使用適配器指定返回對象的類型。

Setting attribute values


每當綁定值發生更改時,綁定類別必須使用綁定表達式在視圖上調用 setter 方法。使用者可以允許 data binding library 自動決定哪個方法,顯式的聲明方法或提供自定義邏輯來選擇方法。

 

Automatic method selection


對於一個命名為 example 的屬性,data binding library 會嘗試尋找名稱為 setExample(arg) 的方法,其方法接受兼容的參數。

只有屬性的名稱和型別會被用於搜尋,屬性的名稱空間則不會被考慮。

例如,對於 android:text="@{user.name}" 表達式,data binding library 會尋找一個 setText(arg) 方法,該方法的參數(arg)的型別為 user.getName 方法所回傳的。例如,若 user.getName 方法回傳的型態為 String,則 data binding library 會尋找 setText 方法,其方法接受的參數型態為 String。若 user.getName 方法回傳的型態為 int,則 data binding library 會尋找 setText 方法,其方法接受的參數型態為 int。

表達式必須回傳正確的型態,如有必要可以進行轉型的動作。即使沒有屬性符合給定的名稱,數據綁定仍然工作。之後,可以使用數據綁定為任何 setter 建立屬性。

例如,類別 DrawerLayout 沒有任何屬性,但有很多 setter。

下方的佈局自動使用 setScrimColor(int) 和 setDrawerListener(DrawerListener) 方法作為 app:scrimColor 和 app:drawerListener 屬性的 setter
<android.support.v4.widget.DrawerLayout  
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">

 

Specify a custom method name


有些屬性的 setter 並不符合名稱,在這種情況下一個屬性可以藉由 BindingMethods 註解去連結一個 setter。

註釋與類別一起使用,可以包含多個 BindingMethod 註釋,每個註釋方法一個註釋。綁定方法可以添加到應用程序中任何類別的註釋。如下方範例

android:tint 屬性和 setImageTintList(ColorStateList) 方法結合,而不是和 setTint 方法結合。
@BindingMethods({  
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})

大多數情況下不需要重命名 setter。已使用名稱約定實現的屬性可自動查找相對應的方法。

 

Provide custom logic


有些屬性需要客製化綁定邏輯。例如 android:paddingLeft 屬性並沒有其相對應的 setter,代替的是 setPadding(left, top, right, bottom) 方法。

一個 static binding adapter 方法加上 BindingAdapter 註釋允許使用者自定義如何調用屬性的 setter。

Android framework 類別的屬性已經有 BindingAdapter 註釋。如下為 paddingLeft 屬性的 binding adapter
@BindingAdapter("android:paddingLeft")  
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}

參數的型態相當重要,第一個參數決定與屬性相關連的視圖型別。第二個參數確定給定屬性的綁定表達式所接受的型別。

Binding adapter 相當有用於其他客製化的類型。如一個客製化的 loader 可以從 worker thread 被呼叫用來讀取圖像。

當發生衝突時,使用者自定義的 binding adapter 會覆蓋過原先的 adapter。

也可以有多個 adapter 用來接收多個屬性。如下
@BindingAdapter({"imageUrl", "error"})  
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.get().load(url).error(error).into(view);
}

如下,可以在 layout 使用 adapter。

注意 @drawable/venueError 參考到 App 中的資源。冒號包括住@{}的寫法為合法的綁定表達式。
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

注意 data binding library 會忽略自定義命名空間以進行匹配。

當 imageUrl 和 error 都被 ImageView 物件使用時,Adapter 將會被呼叫。

若想讓屬性其中之一被使用時就呼叫 adapter,可以設定 requireAll 為 false,如下
@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)  
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}

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 只能與帶有一個抽象方法的接口或抽象類別一起使用。如下
@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);
}
}
}

 

在 layout 中使用這個 event handler,如下
<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

 

當偵聽器具有多個方法時,必須將其拆分為多個偵聽器。

例如 View.OnAttachStateChangeListener 有 2 個方法為 onViewAttachedToWindow(View) 和 onViewDetachedFromWindow(View)。就有 2 個介面去區分它們的 attribute 和 handler。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)  
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}

 

因為更改一個偵聽器也會影響另一個偵聽器,所以需要一個適用於任一屬性或適用於兩者的適配器。

可以透過設定 requireAll 為 false 表示並不是每個屬性都必須指定給表達式。
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)  
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
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);
}
}
};
}

OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}

上面的示例比正常情況稍微複雜一些,因為 View 使用 addOnAttachStateChangeListener()和 removeOnAttachStateChangeListener()方法而不是 OnAttachStateChangeListener 的 setter 方法。

android.databinding.adapters.ListenerUtil 類別有助於跟踪以前的偵聽器,以便可以在綁定適配器中刪除它們。

藉由在 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 方法加入 @TargetApi(VERSION_CODES.HONEYCOMB_MR1),data binding library 只會在裝置運行為 Android3.1 或以上的版本產生 listener。

 

Object conversions


Automatic object conversion


從綁定表達式返回 Object 時,data binding library 會選擇用於設置屬性值的方法。Object 會被強制轉換為所選方法的參數類型。

在使用 ObservableMap 存儲數據的應用程序,此行為很方便,如以下示例所示
<TextView  
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

也可以使用 object.key 的方式在 Map 中參考數值,如上例的 @{userMap["lastName"]} 可以使用 @{userMap.lastName} 來取代。

在表達式中的 userMap 會回傳一個數值,該數值將會被自動轉型為 setText(CharSequence) 方法的參數類型,該方法用來設定 android:text 屬型的値。

如果參數類型不明確,則必須在表達式中強制轉換返回類型。

Custom conversions


在某些情況下,特定類型之間需要自定義轉換。例如 android:background 需要 Drawable,但指定的顏色值是整數。

以下範例顯示需要 Drawable 的屬性,但是提供了一個整數
<View  
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

每當需要 Drawable 並返回一個整數時,int 應該轉換為 ColorDrawable。可以使用帶有BindingConversion 註釋的靜態方法完成轉換,如下所示
@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"/>

 

Orignal From: Binding adapters (綁定轉接器)

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Affiliate Network Reviews