Android DataBinding
Data Binding Library 從 2015 Google I/O 上發布到至今,已經有一年多的長足發展,目前在 Android Studio2.2 版本上已經擁有比較成熟的使用體驗。可以說 Data Binding 已經是一個可用度較高,也能帶來實際生產力提升的技術了。
編譯環境
2.0 版本以后的 Android Studio 已經內置支持了 DataBinding ,我們只要在 gradle 文件中添加如下代碼就可以使用 Databinding:
android {
....
dataBinding {
enabled = true
}
}
xml 文件的處理
<layout>
<data class = "CustomBinding">
</data>
// 原來的layout
</layout>
layout
標簽位于布局文件最外層,可以使原來的普通布局文件轉化為 databinding layout ,同時會在build/ganerated/source/apt
下相關目錄下生成 ***Binding 類
默認生成規則:xml通過文件名生成,使用下劃線分割大小寫,即 activity_main.xml 會生成對應的 ActivityMainBinding
data
標簽用于申明 xml 文件中的變量用于綁定 View,可以通過對標簽的修飾來指定生成 Binding 類的自定義名稱,如上述的布局文件最終會生成一個 CustomBinding 類
Java 代碼的處理
需要用 DataBindingUtil 類中的相關方法取代原先的 setContentView 及 inflate 獲得 ***Binding 實例類
取代findViewById方法
findViewById(int id) 方法是將 View 的實例與 xml 布局文件中的 View 對應賦值的過程,需要遍歷所有的 childrenView 查找。更關鍵的一點是如果比較復雜的頁面,可能會存在數十個控件,光寫 findViewById 也會讓人挺崩潰的。雖說有著諸如 ButterKnife 這樣優秀的第三方庫,但使用數據綁定方式無疑更簡潔明
private TextView mFirstNameTv;
private TextView mLastNameTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(this, R.layout.activity_first);
mFirstNameTv = (TextView) findViewById(R.id.tv_first_name);
mLastNameTv = (TextView) findViewById(R.id.tv_last_name);
}
//********* 或者使用 *********
private ActivityFirstBinding mFirstBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在mBinding中有布局文件中帶id的View變量
mFirstBinding = DataBindingUtil.setContentView(this, R.layout.activity_first);
}
采用 DateBinding 后,所有的 View 會在 Binding 實例類生成對應的實例,而有 id 的 View 則會使用 public 進行修飾,而變量名的生成規則是通過下劃線分割大小寫,即 id = "@+id/main_view"
會生成對應的 mainView 的變量,我們可以直接通過 binding.mainView
獲取,直接節省了在 activity 中聲明一長串變量的步驟,也不需要再寫 findViewById 方法或者加上 @BindView 的注解
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_last_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
在 activity_first.xml 布局文件中添加 databindind 的 layout
標簽后會生成 ActivityFirstBinding 類
// views
private final android.widget.LinearLayout mboundView0;
public final android.widget.TextView tvFirstName;
public final android.widget.TextView tvLastName;
帶 id 的 view 最終會生成 public final 修飾的字段,而不帶 id 的 view 也會生成 private final 修飾的字段。而這些則是在 ActivityLoginBinding 的構造函數中賦值的,僅僅只需要遍歷一遍整個的 view 樹,而不是多個 findViewById 方法遍歷多次
為布局文件綁定Variable
數據綁定getter和setter
Variable 是 DataBinding 中的變量,可以在data
標簽中添加variable
標簽從而在 xml 中引入數據
<layout>
<data>
<variable
name="user"
type="com.sanousun.sh.databinding.bean.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView
android:id="@+id/tv_last_name"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
variable 就是普通的 POJO 類,實現 getter 方法,并沒有提供更新數據刷新 UI 的功能
private static class User {
private String firstName;
private 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;
}
}
如果希望數據變更后 UI 會即時刷新,就需要繼承 Observable 類
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);
}
}
BaseObservable 提供了 notifyChange 和 notifyPropertyChanged 兩個方法來刷新 UI ,前者刷新所有的值,而后者則是刷新 BR 類中有標記的屬性,而 BR 類中的標記生成需要用Bindable
的標簽修飾對應的 getter 方法
同時 databinding 提供了 Observable** 開頭的一系列基礎類可以避免繼承 BaseObservable
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
本質上 Observable** 也是通過繼承 BaseObservable 實現的,調用set方法時會調用 BaseObservable 的 notifyChange 方法
user.firstName.set("first");
String lastName = user.lastName.get();
//********************************
public void set(T value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
運算表達式
運算符
支持絕大部分的 Java 寫法,允許變量數據訪問、方法調用、參數傳遞、比較、通過索引訪問數組,甚至還支持三目運算表達式
- 算術
+
-
*
/
%
- 字符串合并
+
- 邏輯
&&
||
- 二元
&
|
^
- 一元
+
-
!
~
- 移位
>>
>>>
<<
- 比較
==
>
<
>=
<=
- instanceof
- Grouping ()
- 文字 - character, String, numeric, null
- Cast
- 方法調用
- Field 訪問
- Array 訪問 []
- 三目運算符
?:
尚且不支持 this,super,new 以及顯式的泛型調用
空指針處理
無需判斷對象是否為 null,DataBinding 會自動檢查是否為 null,如果引用對象為 null,那么所有屬性引用的都是 null 的,你無需判斷也不會導致崩潰
空合并運算符 ??
引用的對象為 null,需要做額外的判斷,DataBinding 提供了空合并運算
android:text="@{user.firstName ?? user.lastName}"
//會取第一個非空值作為結果,相當于
android:text="@{user.firstName != null ? user.firstName : user.lastName}"
集合數組的調用
對于數組,List,Map,SparseArray的訪問,我們可以直接通過[]
的數組下標來訪問,值得注意的是數組越界的問題
資源文件的引用
值得一說的是可以直接組合字符串
android:text="@{@string/nameFormat(firstName, lastName)}"
<string name="nameFormat">%s, %s</string>
也可以對數值類應用直接進行運算
android:marginLeft="@{@dimen/margin + @dimen/avatar_size}"
需要注意的是一些資源文件需要確切的名稱
Type | Normal Reference | Expression Reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
屬性關聯
DataBinding 庫通過解析 View 的 setter 方法來完成賦值過程,android:text = "@user.firstName"
就相關于調用了
TextView 的 tv.setText(user.firstName)
甚至可以調用 View 未提供的布局屬性,只要 View 提供了對應的 setter 方法。
舉個例子:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"/>
DrawerLayout 有個 setScrimColor(int color)
方法,所以可以在布局中使用未定義的app:scrimColor
屬性,通過 app 命名空間修飾的屬性會自動關聯到對應的方法
屬性擴展
BindingMethods 和 BindingAdapter 注解
但是部分 View 的布局屬性并沒有完整對應的方法提供,比如說 ImageView 的"android:tint"
布局屬性的對應方法是setImageTintList(@Nullable ColorStateList tint)
,這時就需要使用 DataBinding 提供的處理方法,使用BindingMethods
注解
@BindingMethods({
@BindingMethod(type = android.widget.ImageView.class, attribute = "android:tint", method = "setImageTintList"),
@BindingMethod(type = android.widget.ImageView.class, attribute = "android:tintMode", method = "setImageTintMode"),
})
public class ImageViewBindingAdapter {
@BindingAdapter("android:src")
public static void setImageUri(ImageView view, String imageUri) {
if (imageUri == null) {
view.setImageURI(null);
} else {
view.setImageURI(Uri.parse(imageUri));
}
}
@BindingAdapter("android:src")
public static void setImageUri(ImageView view, Uri imageUri) {
view.setImageURI(imageUri);
}
@BindingAdapter("android:src")
public static void setImageDrawable(ImageView view, Drawable drawable) {
view.setImageDrawable(drawable);
}
}
這是系統提供的 ImageViewBindingAdapter,可以在引入了 DataBinding 后全局搜索查看詳情,通過BindingMethod
注解將兩者關聯起來,但是如果 View 甚至沒有實現對應方法或者需要綁定自定義方法,這是可以使用BindingAdapter
注解
BindingConversion 注解
有時在 xml 中綁定的屬性,未必是最后的set方法需要的,比如想用color(int),但是 view 需要 Drawable,比如我們想用String,而 view 需要的是 Url 。這時候就可以使用BindingConversion
注解
<View
android:background=“@{isError ? @color/red : @color/white}”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”/>
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
鏈式表達式
<ImageView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
代碼可以優化成
<ImageView android:id=“@+id/avatar”
android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{avatar.visibility}”/>
<CheckBox android:visibility="@{avatar.visibility}"/>
在系統生成的 Bindinng 類中,會被解析成這三個控件可見性都跟隨著 user.isAdult 的狀態而改變
使用Callback
事件綁定
DataBinding 不僅可以在布局文件中為控件綁定數值,也可以在布局文件中為控件綁定監聽事件
- android:onClick
- android:onLongClick
- android:onTouch
- ......
通常會在java代碼中定義一個名為Handler或者Presenter的類,然后set進來
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable
name="user"
type="com.sanousun.sh.databinding.bean.User"/>
<variable
name="presenter"
type="com.sanousun.sh.databinding.activity.SecondActivity.Presenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_mobile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{presenter::mobileClick}"
android:text="@{user.firstName}"/>
<TextView
android:id="@+id/tv_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->presenter.pwdClick()}"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
在java代碼中:
public class Presenter {
public void mobileClick(View view) {
Toast.makeText(SecondActivity.this, "mobile click", Toast.LENGTH_LONG).show();
}
public void pwdClick() {
Toast.makeText(SecondActivity.this, "pwd click", Toast.LENGTH_LONG).show();
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSecondBinding = DataBindingUtil.setContentView(this, R.layout.activity_second);
mSecondBinding.setUser(new User("da", "shu"));
mSecondBinding.setPresenter(new Presenter());
}
事件綁定使用 lambda 表達式,綁定形式主要是有兩種形式:
Method References
需要方法參數及返回值與對應的 listener 一致,在編譯時生成對應的 listenerImpl 并在放置 presenter 時為對應控件添加監聽,如上面的 mobileClick
// Listener Stub Implementations
public static class OnClickListenerImpl implements android.view.View.OnClickListener{
private com.sanousun.sh.databinding.activity.SecondActivity.Presenter value;
public OnClickListenerImpl setValue(com.sanousun.sh.databinding.activity.SecondActivity.Presenter value) {
this.value = value;
return value == null ? null : this;
}
@Override
public void onClick(android.view.View arg0) {
this.value.mobileClick(arg0);
}
}
代碼中會做 presenter 的空判斷
Listener Bindings
無需匹配對應 listener 的參數,只需要保證返回值的一致即可(除非是void)。與 Method References 的最大的不同點在于
它是在點擊事件發生時相應的
// callback impls
public final void _internalCallbackOnClick(int sourceId , android.view.View callbackArg_0) {
// localize variables for thread safety
// presenter
com.sanousun.sh.databinding.activity.SecondActivity.Presenter presenter = mPresenter;
// presenter != null
boolean presenterObjectnull = false;
presenterObjectnull = (presenter) != (null);
if (presenterObjectnull) {
presenter.pwdClick();
}
}
這個方法會在頁面有點擊時間時調用,同樣也會做空判斷
當然你也可以通過@BindingMethods
和@BindingAdapter
進行自定義的擴展
雙向綁定
有別于單向綁定使用的@{}
符號,雙向綁定使用@={}
符號用于區別,目前支持的屬性有 text,checked,year,month,day,hour,rating,progress 等
InverseBindingListener
實現雙向綁定需要歸功于 DataBinding 庫中的 InverseBindingListener 接口,這個監聽器的作用是監聽目標控件的屬性改變
private android.databinding.InverseBindingListener mboundView1androidCh = new android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of user.male
// is user.setMale((boolean) callbackArg_0)
boolean callbackArg_0 = mboundView1.isChecked();
// localize variables for thread safety
// user.male
boolean maleUser = false;
// user
com.sanousun.sh.databinding.bean.User user = mUser;
// user != null
boolean userObjectnull = false;
userObjectnull = (user) != (null);
if (userObjectnull) {
user.setMale((boolean) (callbackArg_0));
}
}
};
對應 DataBinding 類中有根據雙向綁定生成的 Inverse Binding Event Handlers
@Override
protected void executeBindings() {
......
android.databinding.adapters.CompoundButtonBindingAdapter.setListeners(this.mboundView1, (android.widget.CompoundButton.OnCheckedChangeListener)null, mboundView1androidCh);
}
在綁定時,設置到對應的控件中,當監聽控件屬性改變時,就會觸發重綁定,更新屬性值
InverseBindingMethods 和 InverseBindingAdapter 注解
如果你想做自定義的雙向綁定,你必須充分理解這幾個注解的含義。
@Target({ElementType.ANNOTATION_TYPE})
public @interface InverseBindingMethod {
Class type();
String attribute();
String event() default ""; // 默認會根據attribute name獲取get
String method() default "";// 默認根據attribute增加AttrChanged
}
以系統定義的 CompoundButtonBindingAdapter 為例
@BindingMethods({
@BindingMethod(type = CompoundButton.class, attribute = "android:buttonTint", method = "setButtonTintList"),
@BindingMethod(type = CompoundButton.class, attribute = "android:onCheckedChanged", method = "setOnCheckedChangeListener"),
})
@InverseBindingMethods({
@InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),
})
public class CompoundButtonBindingAdapter {
@BindingAdapter("android:checked")
public static void setChecked(CompoundButton view, boolean checked) {
if (view.isChecked() != checked) {
view.setChecked(checked);
}
}
@BindingAdapter(value = {"android:onCheckedChanged", "android:checkedAttrChanged"},
requireAll = false)
public static void setListeners(CompoundButton view, final OnCheckedChangeListener listener,
final InverseBindingListener attrChange) {
if (attrChange == null) {
view.setOnCheckedChangeListener(listener);
} else {
view.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (listener != null) {
listener.onCheckedChanged(buttonView, isChecked);
}
attrChange.onChange();
}
});
}
}
}
雙向綁定需要為屬性綁定一個監聽器,這里就是需要為"android:checked"
屬性綁定監聽器,通過 @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),databinding 可以通過 checkedAttrChanged 找到 OnCheckedChangeListener,設置 OnCheckedChangeListener 來通知系統生成的 InverseBindingListener 調用 onChange 方法,從而通過 getter 方法來獲取值。值得注意的是為了防止無限循環調用,setter 方法必須要去進行重判斷
同樣如果沒有對應方法,可以自定義 InverseBindingAdapter 來實現,詳情見系統TextViewBindingAdapter
隱式調用
實現了雙向綁定的屬性就可以隱式調用,而不用寫繁瑣的 listener
<CheckBox android:id="@+id/cb"/>
<ImageView android:visibility="@{cb.checked ? View.VISIBLE : View.GONE}"/>
屬性改變監聽
當然我們可以通過 Observable.OnPropertyChangedCallback 來監聽屬性的改變,從而實現具體的業務邏輯
user.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
if (i== BR.firstName){
Toast.makeText(ThirdActivity.this, user.getFirstName(), Toast.LENGTH_LONG).show();
}
}
});
RecyclerView的處理
只要簡單的定義 ViewHolder
public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
protected final T mBinding;
public BindingViewHolder(T binding) {
super(binding.getRoot());
mBinding = binding;
}
public T getBinding() {
return mBinding;
}
}
因為邏輯和屬性的綁定在xml中就已經處理好,adapter 的創建變得十分的容易,一般情況下可以直接使用,如果需要額外的更改可以繼承。而點擊事件的監聽可以在 onBindViewHolder 中設置
對于含有多種 viewType 的列表適配器,在不同 xml 布局文件中 variable 的 name 可以全部寫為 item,那么在綁定數據時
無需特殊處理
@Override
public void onBindViewHolder(BindingViewHolder holder, int position) {
final Data data = mData.get(position);
holder.getBinding().setVariable(item, data);
holder.getBinding().executePendingBindings();
}
在生成的代碼中會去檢查它的類型,并將其賦值
高級用法
component 注入
Data Binding Component詳解 - 換膚什么的只是它的一個小應用!
原理簡述
解析
編譯時,系統會將 xml 文件拆分為兩部分,數據部分的 xml 和布局部分的 xml,分別存放于app/build/intermediates/data-binding-info
和app/build/intermediates/data-binding-layout-out
之中,數據部分的 xml 文件記錄 view 對應的賦值表達式,而布局部分的 xml 則是普通的布局如下
<Button
android:id="@+id/btn_btn"
android:layout_width="match_parent"
android:layout_height="56dp"
android:tag="binding_1"/>
特殊在于每個控件都會生成 tag,作用是生成 DataBinding 時可以綁定對應控件,因此在布局文件中需要避免書寫tag
解析xml -> 解析表達式 -> java編譯 —> 解析依賴 -> setter
public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
super(bindingComponent, root, 1);
final Object[] bindings = mapBindings(bindingComponent, root, 4, sIncludes, sViewsWithIds);
this.activityMain = (android.widget.LinearLayout) bindings[0];
this.activityMain.setTag(null);
this.btnBtn = (android.widget.Button) bindings[1];
this.btnBtn.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
在生成的 binding 類中,構造函數會為所有的控件賦值,此時會將 tag 值去除,所以說為 View 的賦值需要在獲取 DataBinding 實例之后。初始化時遍歷 view 賦值比 findViewById 效率高得多
綁定
綁定的代碼都在生成的 DataBinding 類中的 executeBindings 方法中,不管任何涉及到更新 ui 的地方最終都會調用這個方法
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
//一些變量的定義
......
if ((dirtyFlags & 0x5L) != 0) {
//根據flag的值判斷是否需要做相應的改變
......
}
......
}
databinding 使用位標記來檢驗更新(dirtyFlags),每一個標志位都有自己的含義,生成的規則由內部解析表達式后確定,在ViewDataBinding 中我們可以看到
if (USE_CHOREOGRAPHER) {
mChoreographer = Choreographer.getInstance();
mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mRebindRunnable.run();
}
};
} else {
mFrameCallback = null;
mUIThreadHandler = new Handler(Looper.myLooper());
}
批量刷新會發生在系統的幀布局刷新時,系統幀布局刷新回調 -> mRebindRunnable -> executePendingBindings -> executeBindings,此時才會觸發數據更改的操作
更新
刷新布局最終都會調用 executeBindings 方法,而在父類 ViewDataBinding 類是由 executePendingBindings 調用方法,我們可以直接調用此方法來加載掛起的屬性變更,而不用等待下一次的幀布局刷新
而所有的 Variable 內部屬性的改變則會注冊監聽器,監聽改變 -> handleFieldChange -> requestRebind -> executePendingBindings -> executeBindings 最終改變屬性
參考
從零開始的Android新項目7 - Data Binding入門篇
棉花糖給 Android 帶來的 Data Bindings(數據綁定庫)