Android DataBinding入門

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-infoapp/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(數據綁定庫)

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

推薦閱讀更多精彩內容