Data Binding MVVM 數據綁定 總結


官方教程: Data Binding Guide      API   

關於 Data Binding

Data Binding 解決了 Android UI 編程的一個痛點,官方原生支持 MVVM 模型可以讓我們在不改變既有代碼框架的前提下,非常容易地使用這些新特性。

Data Binding 框架如果能夠推廣開來,也許 RoboGuice、ButterKnife 這樣的依賴注入框架會慢慢失去市場,因為在 Java 代碼中直接使用 View 變量的情況會越來越少。


【使用條件】
Data Binding庫不僅靈活而且廣泛兼容:它是一個support庫,因此你可以在所有的Android平台最低能到Android 2.1(API等級7+)上使用它。
Gradle 1.5 alpha 及以上自帶支持 DataBinding,僅需在使用 DataBinding 的 module 里面的 build.gradle 里面加上配置即可:
android {
    dataBinding {
        enabled true
    }
}

【利弊】
優勢
  • DataBinding 出現以前,我們在實現 UI 界面時,不可避免的編寫大量的毫無營養的代碼:比如 View.findViewById(),比如各種更新 View 屬性的 setter:setText(),setVisibility(),setEnabled() 或者 setOnClickListener() 等等。
  • 這些“垃圾代碼”數量越多,越容易滋生 bug。
  • 使用 DataBinding,我們可以避免書寫這些“垃圾代碼”。
劣勢
  • 使用 Data Binding 會增加編譯出的 apk 文件的類數量和方法數量。
  • 新建一個空的工程,統計打開 build.gradle 中 Data Binding 開關前后的 apk 文件中類數量和方法數量,類增加了 120+,方法數增加了 9k+(開啟混淆后該數量減少為 3k+)。
  • 如果工程對方法數量很敏感的話,請慎重使用 Data Binding。

基礎功能的完整案例

【編寫布局文件】

使用 Data Binding 之后,xml 的布局文件就不再用於單純地展示 UI 元素,還需要定義 UI 元素用到的變量。所以,它的根節點不再是一個 ViewGroup,而是變成了 layout,並且新增了一個節點 data。

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data> …  </data>  //新增的data節點 
    
    <LinearLayout> ... </LinearLayout>  //原先的根節點
</layout>

要實現 MVVM 的 ViewModel 就需要把數據(Model)與 UI(View) 進行綁定,data 節點的作用就像一個橋梁,搭建了 View 和 Model 之間的通路。


【定義數據對象】

定義一個 POJO 類:

public class User {
    private String firstName;//就是一個簡單的Java Bean
    private String lastName;
    ...
}

稍后,我們會在 xml 布局文件的 data 節點中聲明一個 User 類型的 variable,這個變量會為 UI 元素提供數據,然后在 Java 代碼中把『后台』數據與這個 variable 進行綁定。


【布局中定義 variable】

回到布局文件,在 data 節點中聲明一個 User 類型的變量 user。

<data>
    <variable name="user" type="com.bqt.basic.User" />
</data>

其中 type 屬性就是我們剛剛定義的 User 類。

當然,data 節點也支持 import,並且 import 並沒有要求一定要放在使用前,所以上面的代碼可以這樣寫:

<data>
    <import type="com.bqt.basic.User" />
    <variable name="user" type="User" />
</data>

注意:

  • java.lang.* 包中的類會被自動導入,可以直接使用。
  • 基本類型的type可以使用包裝類或直接使用原型,比如,整數可以使用【type="int"】或type="Integer"】


Java代碼中創建** Binding類

我們 build 工程后會自動在 build 目錄下生成一個繼承自 ViewDataBinding 的類,這個類將被放置在databinding包下。比如,如果我們的包名是com.bqt.databinding,那么它將被放置在com.bqt.databinding.databinding包下。


默認情況下, **Binding類的命名是基於layout文件的名稱的,用大寫開頭,除去下划線並將下划線后的首字母大寫,然后添加“Binding”后綴。 例如,這里 xml 的文件名叫 activity_basic.xml,那么 自動 生成的類就是 ActivityBasic Binding

創建bindings的最簡單的方式是在inflating(注:layout文件與Activity/Fragment的“鏈接”)期間,比如:
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //用 DatabindingUtil.setContentView() 來替換掉 setContentView()
    MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
    ...
}

【Java代碼中綁定variable】

在創建**Binding類的實例后,我們便可以使用它來綁定variable,例如:

User user = new User("Test", "User");
binding.setUser(user);

注意,所有的 set/get 方法都是根據 variable 名稱生成的,例如,上面布局中定義了一個 name="user" 的變量:

那么就會生成對應的 set/get 方法:

public void setUser(com.bqt.databinding.model.User User) { ... }
public com.bqt.databinding.model.User getUser() { ... }


【布局中使用variable】

數據與variable 綁定之后,通過 @{} 可以直接把 Java 中定義的屬性值賦值給 xml 中 UI 元素的某個屬性。

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

至此,一個簡單的數據綁定就完成了。


Observable Binding

任何Plain old Java object(POJO)可用於Data Binding,但修改POJO不會導致UI更新。比如在上面我們的案例中,我們通過 binding.setUser(user) 將數據 User 和 View 綁定在了一起,我們本想着,在 User 改變之后, View 會自動更新,但實際上是沒有這種效果的。


Data Binding的真正能力是當數據變化時,可以通知給你的Data對象。有三種不同的數據變化通知機制:Observable對象、ObservableFields以及Observable集合當這些可觀察Data對象綁定到UI,Data對象屬性的更改后,UI也將自動更新。


Observable和BaseObservable

實現 android.databinding.Observable 接口的類可以允許添加一個監聽器到 Bound 對象以便監聽對象上的所有屬性的變化。Observable 接口有一個機制可以添加和刪除監聽器,但通知與否由開發人員管理。

為了使開發更容易,一個 BaseObservable 的基類為實現監聽器注冊機制而創建。Data實現類依然負責通知當屬性改變時。這是通過指定一個@Bindable注解給getter以及setter內通知來完成的。


【接口Observable】
Observable classes provide a way in which data bound UI(數據綁定UI) can be notified of changes. ObservableList and ObservableMap also provide the ability to notify when changes occur. ObservableField, ObservableParcelable, ObservableBoolean, ObservableByte, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, and ObservableDouble provide a means by which properties may be notified without implementing Observable.

An Observable object should notify the Observable.OnPropertyChangedCallback whenever an observed property of the class changes.
The getter for an observable property should be annotated with Bindable.
Convenience class BaseObservable implements this interface and PropertyChangeRegistry can help classes that don't extend BaseObservable to implement the listener registry.

【內部類OnPropertyChangedCallback】
The callback that is called by Observable when an observable property has changed.
abstract void   onPropertyChanged(Observable sender, int propertyId): Called by an Observable whenever an observable property changes.

【實現類BaseObservable】
A convenience class that implements Observable interface and provides notifyPropertyChanged(int) and notifyChange() methods.
BaseObservable的已知子類:8個基本數據類型對應的 Observable 類,以及一個ObservableField
  • ObservableField<T>
    • ObservableParcelable<T extends Parcelable>
  • ObservableBoolean, ObservableByte, ObservableChar, ObservableDouble, ObservableFloat, ObservableInt, ObservableLong, ObservableShort

BaseObservable的API
  • [擴充的方法] synchronized void  notifyChange():Notifies listeners that all properties of this instance have changed.
  • [擴充的方法] void    notifyPropertyChanged(int fieldId):Notifies listeners that a specific property has changed.
  • [實現的方法] synchronized void  addOnPropertyChangedCallback(Observable.OnPropertyChangedCallback callback):Adds a callback to listen for changes to the Observable.
  • [實現的方法] synchronized void   removeOnPropertyChangedCallback(Observable.OnPropertyChangedCallback callback):Removes a callback from those listening for changes.
/**
 * Notifies listeners that a specific property has changed. The getter for the property
 * that changes should be marked with @Bindable to generate a field in BR to be used as fieldId.
 * @param fieldId The generated BR id for the Bindable field.
 */
public void notifyPropertyChanged(int fieldId) {
    synchronized (this) {
        if (mCallbacks == null) return;//private transient PropertyChangeRegistry mCallbacks;
    }
    mCallbacks.notifyCallbacks(this, fieldId, null);//通過【PropertyChangeRegistry】來實現通知監聽器
}

【使用示例】
public class ObservableUser extends BaseObservable {
	private String name;
	
	@Bindable//給getter方法添加注解。The getter for an observable property should be annotated with Bindable.
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
		notifyPropertyChanged(BR.name);//通知某個屬性改變了。Notifies listeners that a specific property has changed.
	}
}

BR 是編譯階段生成的一個類,功能與 R.java 類似。

在編譯期間,通過@Bindable注解標記的getter方法返回的字段會在BR類文件中生成一個同名的Entry,如下:

package com.bqt.databinding;
public class BR {
    ...
    public static final int name = 17;
    ...
}

Observable field

除此之外,還有一種更細粒度的綁定方式,可以具體到成員變量,這種方式無需繼承 BaseObservable,一個簡單的 POJO 就可以實現。

系統為我們提供了所有的原始類型所對應的 Observable 類,例如ObservableInt、ObservableFloat、ObservableBoolean等等,還有一個 ObservableField 對應着 引用類型。這種方式非常適合那些幾乎沒有幾個屬性的 POJO 。

public class android.databinding.ObservableInt extends BaseObservable implements Parcelable Serializable

ObservableInt:An observable class that holds a primitive int.

ObservableField:An object wrapper to make it observable.

Observable field classes may be used instead of creating an Observable object. 

Fields of this type should be declared final because bindings only detect檢測 changes in the field's value, not of the field itself.

This class is parcelable可擴展的 and serializable but callbacks are ignored when the object is parcelled / serialized. Unless you add custom callbacks, this will not be an issue because data binding framework always re-registers callbacks when the view is bound.


要使用它需要在data對象中創建public final字段,如:

public class PlainUser {
	public final ObservableField<String> name = new ObservableField<>();
	public final ObservableInt age = new ObservableInt();
}

要訪問該值,使用set和get方法:

PlainUser plainUser = new PlainUser();
plainUser.name.set("包青天,ObservableField");
plainUser.age.set(27);

int age = plainUser.age.get();


Observable集合

一些app使用更多的動態結構來保存數據,Observable集合允許鍵控訪問這些data對象。

【ObservableArrayMap】

public class android.databinding.ObservableArrayMap<K, V> extends ArrayMap<K, V> implements ObservableMap<K, V>

ObservableArrayMap用於鍵是引用類型,如String。

ObservableArrayMap<String, Object> mapUser = new ObservableArrayMap<>();
mapUser.put("name", "ObservableArrayMap");
mapUser.put("age", 27);

在layout文件中,通過String鍵可以訪問map。

android:text='@{mapUser["name"]}'
android:text='@{String.valueOf(1 + (Integer)mapUser["age"])}'

注意,在XML布局的某個屬性值中使用泛型時,要用【&lt;】來代替【<】


【ObservableArrayList】
public class android.databinding.ObservableArrayList<T> extends ArrayList<T> implements ObservableList<T>
ObservableArrayList用於鍵是整數,如:
ObservableArrayList<Object> listUser = new ObservableArrayList<>();
listUser.add("ObservableArrayList");
listUser.add(17);

listUser.set(0, "包青天,ObservableArrayList");
listUser.set(1, 27);

在layout文件中,通過索引可以訪問list:

<import type="android.databinding.ObservableList"/>
<variable name="listUser" type="ObservableList&lt;Object>"/>

android:text='@{listUser[0]}'
android:text='@{String.valueOf(1 + (Integer)listUser[1])}'


雙向綁定

在布局中引用variable時,將之前的【@{}】改成了【@={}】即可實現雙向綁定,比如通過【@={}】將一個變量和EditText的內容綁定在一起,當用戶更改EditText中的內容時,和它綁定的變量也會同步改變。

android:text="@{user.name}"//通過【 @{user.name} 】方式綁定時,當用戶更改EditText中的內容時,和它綁定的變量【不會】同步改變
android:text="@={user2.name}"//通過【 @={user.name} 】方式綁定時,當用戶更改EditText中的內容時,和它綁定的變量【會】同步改變


常用表達式

常用表達式跟Java表達式很像,以下這些是一樣的:

  • 數學 + - / * %
  • 字符串連接 +
  • 邏輯 && ||
  • 二進制 & | ^
  • 一元運算 + - ! ~
  • 移位 >> >>> <<
  • 比較 == > < >= <=
  • instanceof
  • 分組 ()
  • null
  • Cast
  • 方法調用
  • 數據訪問 []
  • 三元運算 ? :

缺少的操作:

  • this
  • super
  • new
  • 顯式泛型調用

示例:

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


集合元素的訪問

常用的集合:arrays、lists、sparse lists以及maps,為了簡便都可以使用 [] 來訪問

<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]}"

注意,在XML布局的某個屬性值中使用泛型時,要用轉義字符【&lt;】來代替【<】,否則編譯失敗:

> org.xml.sax.SAXParseException; 與元素類型 "variable" 相關聯的 "type" 屬性值不能包含 '<' 字符。

可以使用單引號包含屬性值,而在表達式中使用雙引號

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

也可以使用雙引號來包含屬性值,在字符串前后使用【`】,這個是ESC下面、Tab上面的那個鍵

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

也可以使用雙引號來包含屬性值,在字符串前后使用轉義字符【&quot;】

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

PS,XML中需要的轉義字符:

&(邏輯與)  &amp;
<(小於)    &lt;
>(大於)    &gt;
"(雙引號)  &quot;
'(單引號)  &apos;


使用資源數據 Resources

使用正常的表達式來訪問resources也是可行的:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
//dimens.xml中的定義
<dimen name="largePadding">20dp</dimen>
注意,官方教程中以下訪問方式是錯誤的,會導致此View顯示不出來:
android:padding="@{large ? (int)@dimen/largePadding : (int)@dimen/smallPadding}"  //錯誤寫法
格式化字符串和復數可以通過提供參數來判斷:
android:text="@{@string/nameFormat(firstName, lastName)}"

<string name="nameFormat">Full Name: %1$s %2$s</string> //格式化字符串

其他一些小知識點

【使用靜態成員】
android:text="@{MyStringUtils.upper(user.firstName)}"
android:text="@{User.SEX}"
【使用類型別名】
<import type="com.examle.detail.data.User" alias="DetailUser" />
<variable name="user" type="DetailUser" />
【自定義Binding類】
除了使用框架自動生成的 ** Binding ,我們也可通過調整data元素中的class屬性來重命名或放置在不同的包中 ,如:
<data class=".CustomBinding"> ... //放在package的根目錄,即上述databinding的父目錄
<data class="com.mypackage.CustomBinding"> ... //提供整個包名

【Null合並操作】

??運算符:左邊的對象如果它不是null,選擇左邊的對象;或者如果它是null,選擇右邊的對象:

android:text="@{user.displayName ?? user.lastName}"
//等價於:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"

【帶 ID 的 View】

只要給 View 定義一個 ID,Data Binding 就會為我們生成一個對應的 final 字段。Binding在View層次結構上做單一的傳遞,提取帶ID的Views。這種機制比起某些Views使用findViewById還要快。例如:

android:id="@+id/firstName"
binding.firstName.setText("包");//firstName是DataBinding自動生成一個對應的變量

【使用 include】

屬性中的 variable 名字從容器 layout 中傳遞到被包含的 layout。

注意:在include的layout文件中定義的variable必須在外部也要定義,比如下例中,在 user.xml 中必需要有 user variable

<include layout="@layout/layout_input"/>
<include
    layout="@layout/user"
    bind:user="@{user}"/>

避免 NullPointerException

Data Binding代碼生成時自動檢查是否為nulls來避免出現NullPointerException錯誤。

例如,在表達式 @{user.name} 中,如果user是null,則 user.name 會賦予它的默認值(null)。如果你引用 user.age(age是int類型),那么它的默認值是0。


【直接Binding】

當一個variable或observable變化時,binding會在下一幀之前被計划要改變。有很多次,但是在Binding時必須立即執行。要強制執行,使用executePendingBindings()方法。

Evaluates評估 the pending bindings, updating any Views that have expressions bound to modified variables. This must be run on the UI thread.


【后台線程】

只要它不是一個集合,你可以在后台線程中改變你的數據模型。在判斷是否要避免任何並發問題時,Data Binding會對每個Varialbe/field本地化。


使用ViewStubs

ViewStubs跟正常的Views略有不同,他們開始時是不可見的,當他們被設置為可見或被明確告知要載入時,它們通過載入另外一個layout取代了自己。

xml 文件與之前的代碼一樣,根節點改為 layout,在 LinearLayout 中添加一個 ViewStub,添加 ID。

<ViewStub
    android:id="@+id/m_view_stub"
    android:layout="@layout/m_view_stub"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

編譯后會在 Binding 中生成同名的 ViewStubProy 類型的成員:

public final android.databinding.ViewStubProxy mViewStub;

在 Java 代碼中,通過 Binding 實例為 ViewStubProy 注冊 ViewStub.OnInflateListener 事件,當監聽到ViewStub的OnInflateListener事件時需要為新的布局創建一個Binding:

mBinding.mViewStub.setOnInflateListener(new ViewStub.OnInflateListener() {//監聽ViewStub的OnInflateListener監聽器
    @Override
    public void onInflate(ViewStub stub, View inflated) {//當載入另一個layout時為新的布局創建一個Binding
        MViewStubBinding binding = DataBindingUtil.bind(inflated);//同樣,此Binding的名字取決於ViewStub布局的名字。
        User user = new User("fee", "lang");
        binding.setUser(user);//此Binding只能處理ViewStub布局中帶id的View或變量,而不能處理根布局中的東西
    }
});

//填充ViewStub
if (!mBinding.mViewStub.isInflated())  mBinding.mViewStub.getViewStub().inflate();

動態Variables,如RecyclerView

有時不知道具體的Binding類, 以 RecyclerView 為例,Adapter 的 DataBinding 需要動態生成,因此我們可以在 onCreateViewHolder 的時候創建這個 DataBinding,然后在 onBindViewHolder 中獲取這個 DataBinding。
static class UserAdapter2 extends RecyclerView.Adapter<UserAdapter2.UserHolder> {
	
	private List<User> mUsers;
	
	public UserAdapter2(List<User> mUsers) {
		this.mUsers = mUsers;
	}
	
	static class UserHolder extends RecyclerView.ViewHolder {
		private ViewDataBinding binding;
		
		public UserHolder(View itemView) {
			super(itemView);
		}
		
		public ViewDataBinding getBinding() {
			return binding;
		}
		
		public void setBinding(ViewDataBinding binding) {
			this.binding = binding;
		}
	}
	
	@Override
	public int getItemCount() {
		return mUsers.size();
	}
	
	@Override
	public UserHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
		ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()),
				R.layout.user_item, viewGroup, false);//【在 onCreateViewHolder 的時候創建這個 DataBinding】
		UserHolder holder = new UserHolder(binding.getRoot());
		holder.setBinding(binding);
		return holder;
	}
	
	@Override
	public void onBindViewHolder(UserHolder holder, int position) {
		ViewDataBinding binding = holder.getBinding();//【在 onBindViewHolder 中獲取這個 DataBinding】
		binding.setVariable(BR.user, mUsers.get(position));
		binding.executePendingBindings();
	}
}

還有另外一種比較簡潔的方式,直接在構造 Holder 時把 View 與自動生成的 XXXBinding 進行綁定。

static class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserHolder> {
	
	private List<User> mUsers;
	
	public UserAdapter(List<User> mUsers) {
		this.mUsers = mUsers;
	}
	
	static class UserHolder extends RecyclerView.ViewHolder {//最主要的區別就是在這里!
		private UserItemBinding mBinding;
		
		public UserHolder(View itemView) {
			super(itemView);
			mBinding = DataBindingUtil.bind(itemView);//【在構造 Holder 時把 itemView 與自動生成的 XXXBinding 進行綁定】
		}
		
		public void bind(User user) {
			mBinding.setUser(user);//綁定數據
		}
	}
	
	@Override
	public int getItemCount() {
		return mUsers.size();
	}
	
	@Override
	public UserHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
		View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.user_item, viewGroup, false);
		return new UserHolder(itemView);
	}
	
	@Override
	public void onBindViewHolder(UserHolder holder, int position) {
		holder.bind(mUsers.get(position));//只需要為每一個item綁定數據,而不需要手動操作item中的UI
	}
	
}

屬性 Setters

每當綁定值的變化,生成的Binding類必須調用 setter 方法。Data Binding框架有可以自定義賦值的方法。

自動Setters

對於一個屬性,Data Binding 試圖找到  setAttribute 方法,與該屬性的  namespace 並沒什么關系, 僅僅與屬性本身名稱有關例如,有關TextView的 android:text 屬性的表達式會尋找一個 setText(String) 的方法。
您可以通過Data Binding輕松地 為任何setter"創造"屬性例如,DrawerLayout沒有任何屬性,但大量的setters,您可以使用自動setters來使用其中的一個。
<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

同樣,對於自定義View,即使屬性沒有在 declare-styleable 中定義,我們也可以通過 xml 進行賦值操作。
為了演示這個功能,我自定義了一個 View - NameCard,屬性資源 R.styleable.NameCard 中只定義了一個 age 屬性:

<resources>
    <declare-styleable name="NameCard">
        <attr name="age" format="integer" />
    </declare-styleable>
</resources>

其中 firstName 和 lastName 有對應的兩個 setter 方法(前提條件)

public class NameCard extends LinearLayout {
    private TextView mFirstName, mLastName;
    public void setFirstName(@NonNull final String firstName) {
		mFirstName.setText(firstName);
	}

	public void setLastName(@NonNull final String lastName) {
		mLastName.setText(lastName);
	}
    
    public void setAge(@IntRange(from = 1) int age) {
		mAge = age;
	}
    ...
}

只要有 setter 方法就可以像下面代碼一樣賦值:

<com.bqt.databinding.NameCard
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:onClickListener="@{activity.clickListener}"
    app:firstName="@{@string/firstName}"
    app:lastName="@{@string/lastName}"
    app:age="27" />

onClickListener 也是同樣道理(因為任何View都有對應的setOnClickListener方法),只不過我們是在 Activity 中定義了一個 Listener。


重命名Setters

使用BindingMethods重命名Setters
兩個注解的介紹:
  • BindingMethodUsed within an BindingMethods annotation to describe a renaming of an attribute to the setter used to set that attribute. By default, an attribute attr will be associated with setter setAttr.
  • BindingMethodsUsed to enumerate attribute-to-setter renaming. By default, an attribute is associated with setAttribute setter. If there is a simple rename, enumerate them in an array of BindingMethod annotations in the value.
兩個注解的定義:
@Target(ElementType.ANNOTATION_TYPE)
public @interface BindingMethod {
    Class type();//The View Class that the attribute is associated with.
    String attribute();//The attribute to rename. Use android: namespace for all android attributes or no namespace for application attributes.
    String method();//The method to call to set the attribute value.
}

@Target({ElementType.TYPE})
public @interface BindingMethods {
    BindingMethod[] value();
}
一些屬性雖然擁有setters但是並不與名字相匹配,這些方法的屬性可以通過 @BindingMethod && @BindingMethods 注釋 setters。 這必須與一個包含 BindingMethod 注解的類相關聯,每一個用於一個重命名的方法。例如,android:tint 屬性與 setImageTintList 相關聯,而不與setTint相關。
如下為TextViewBindingAdapter中的BindingMethods注解:
@BindingMethods({
        @BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
        @BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
        ...
})
public class TextViewBindingAdapter { ... }
開發人員不太可能需要重命名 setters ,因為android框架屬性已經實現了這一部分。

自定義Setters

@BindingAdapter注解簡介

注解的定義:
@Target(ElementType.METHOD)
public @interface BindingAdapter {
    String[] value();//The attributes associated with this binding adapter.
    boolean requireAll() default true;
}
BindingAdapter is applied to methods that are used to manipulate操作 how values with expressions are set to views. The simplest example is to have a public static method that takes the view and the value to set:
@BindingAdapter("android:bufferType")
 public static void setBufferType(TextView view, TextView.BufferType bufferType) {
     view.setText(view.getText(), bufferType);
 }
In the above example, when android:bufferType is used on a TextView, the method setBufferType is called.

It is also possible to take previously set values, if the old values are listed first:
@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);//再添加新的
    }
}

When a binding adapter may also take multiple attributes, it will only be called when all attributes associated with the binding adapter have binding expressions associated with them. This is useful when there are unusual interactions between attributes. For example:
@BindingAdapter({"android:onClick", "android:clickable"})
public static void setOnClick(View view, View.OnClickListener clickListener, boolean clickable) {
    view.setOnClickListener(clickListener);
    view.setClickable(clickable);
}
The order of the parameters must match the order of the attributes in values in the BindingAdapter.

A binding adapter may optionally可選的 take a class extending DataBindingComponent as the first paramet er as well. If it does, it will be passed the value passed in during binding, either directly in the inflate method or indirectly, using the value from getDefaultComponent().
If a binding adapter is an instance method, the generated DataBindingComponent will have a getter to retrieve an instance of the BindingAdapter's class to use to call the method.

總結:
@BindingAdapter 是一個注解,用來標記 public static 方法 ,這些方法的目的是給 view 設置屬性值,利用此注解可以自定義屬性的setter操作。

系統提供的示例

Android的屬性已經創造了大量的 BindingAdapters ,你可以在項目module的  android.databinding.adapters  包下找到這些類。
 
以下是 TextViewBindingAdapter 中的部分代碼
public class TextViewBindingAdapter {
    @BindingAdapter({"android:bufferType"})
    public static void setBufferType(TextView view, TextView.BufferType bufferType) {
        view.setText(view.getText(), bufferType);
    }
   
   private static void setIntrinsicBounds(Drawable drawable) {
        if (drawable != null) drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
    }

    @BindingAdapter({"android:drawableBottom"})
    public static void setDrawableBottom(TextView view, Drawable drawable) {
        setIntrinsicBounds(drawable);//設置邊界
        Drawable[] drawables = view.getCompoundDrawables();
        view.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawable);//修改DrawableBottom
    }
}

自定義案例一

有些屬性需要自定義綁定邏輯,例如,對於 android:paddingTop 屬性並沒有相關setter,相反,setPadding(left, top, right, bottom)是存在的,一個帶有  BindingAdapter  注解的 靜態綁定適配器 方法允許開發者 自定義setter如何對於一個屬性的調用
第一步,在xml中定義一個variable,並將其設置為某個View的  android:paddingTop 屬性值(即在布局文件中引用這個方法)
<variable name="pt" type="int"/>
android:paddingTop="@{pt}"
現在我們這個View已經和model(即我們定義的variable pt)綁定了。
第二步,重新定義 android:paddingTop 屬性的 setter 方法
@BindingAdapter({"android:paddingTop"})
public static void setPaddingTopAndBottom(View view, int paddingTB) {//當設置paddingTop時,同時設置paddingTop和paddingBottom
	view.setPadding(view.getPaddingLeft(), paddingTB, view.getPaddingRight(), paddingTB);
}
第三步,通過Binding改變variable pt的值。
mBinding.setPt(10 * new Random().nextInt(10));
可以發現, 當改變 pt的值時, View的paddingTop和paddingBottom同時改變了

自定義案例二

Binding適配器對其他定制類型非常有用,例如,自定義 loader 可以用來異步載入圖像:
<com.liangfeizc.avatarview.AvatarView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:error="@{@drawable/error}"
    app:imageUrl="@{imageUrl}"
    app:onClickListener="@{activity.mClickListener}"/>
@BindingAdapter({"bind:imageUrl", "error"})//自定義 namespaces 將被忽略
public static void loadBqtImage(ImageView view, String url, Drawable error) {//方法名隨意,但參數必須和注解中指定的屬性一一對應
	Picasso.with(view.getContext()).load(url).error(error).into(view);
}
如果對於一個ImageView,imageUrl和error都被使用,並且 imageUrl 是一個 String 類型以及 error 是一個 Drawable 類型時,該適配器會被調用。
注意:
  • 匹配過程中自定義 namespaces 將被忽略,你可以在@BindingAdapter中加任何 namespaces,但是會有如下提示:
Error:(37, 21) 警告: Application namespace for attribute bind:imageUrl will be ignored.
  • 允許重寫android的命名空間。
  • 當你創建的適配器屬性與系統默認的產生沖突時,你的自定義適配器將會覆蓋掉系統原先定義的注解,這將會產生一些意外的問題。


轉換器 @BindingConversion

對象轉換

當從Binding表達式返回一個對象時,一個setter會從 自動 setters 、重命名 setters 以及自定義的setters中選擇,且該對象將被轉換為所選擇的setter的參數類型。 這是為了方便那些使用ObservableMaps來保存數據。例如:
<TextView
  android:text='@{userMap["lastName"]}'
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>
在userMap返回一個對象后該對象將 自動轉換為setText(CharSequence)的參數類型。
當有關參數類型可能混亂時,開發人員需要在表達式中轉換。

@BindingConversion注解簡介

Annotate methods that are used to automatically convert from the expression type to the value used in the setter. The converter should take one parameter, the expression type, and the return value should be the target value type used in the setter. Converters are used whenever they can be applied and are not specific to any attribute.
用於注解那些自動將表達式類型轉換為setter中使用的值的方法。 轉換器應該采用一個參數,表達式類型和返回值應該是setter中使用的目標值類型。只要滿足條件,就會應用轉換器,並且不特定於任何屬性。
@Target({ElementType.METHOD})
public @interface BindingConversion {
}

自定義轉換

在 xml 中為屬性賦值時,如果變量的類型與屬性不一致,通過 DataBinding 可以進行轉換。 例如,設置背景的時候:
<View
  android:background="@{isError ? @color/red : @color/white}"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>
這里的background需要一個 Drawable 類型的對象,但顏色卻是一個整數。
不過,我們通過提供一個帶有@BindingConversion注解的靜態方法,便可以完成的如下功能:
不管何時,如果某個屬性對應的setter方法需要一個Drawable,但返回值(用戶設置的值)是一個整數,那么整數類型會被轉換為 ColorDrawable
@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"/>

以下非常重要!
使用 Converter 一定要保證它不會影響到其他的屬性,例如下面這個 convertColorToString 就會影響到 android:visibility,因為他們都都符合從 int 到 int 的轉換。
/**
 * !!! Binding conversion should be forbidden, otherwise it will conflict with{@code android:visiblity} attribute.
 */
@BindingConversion
public static int convertColorToString(int color) {
	switch (color) {
		case Color.RED:
			return R.string.red;
		case Color.WHITE:
			return R.string.white;
	}
	return R.string.app_name;
}

2017-9-27






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM