Android Data Binding——高級

上一篇文章Android Data Binding——進階
介紹了Data Binding的語法等進階功能。這一篇我們來介紹一下Data Binding的數(shù)據(jù)對象。

文中的例子可前往DataBindingDemo查看。

任何POJO對象都可以用在data binding中,但是對象改變時候,要如何通知UI更新呢?這是使用Data Binding最奧妙的地方。Ps:我們這邊只是介紹如何使用,沒有涉及到實現(xiàn)原理。

有三種不同的數(shù)據(jù)變化通知機制:observable objects, observable fields, and observable collections.

這些observable對象綁定到UI上,當對象的屬性更改時就會自動通知UI更新。

Observale Objects

一個繼承Observable接口的類,data binding會設(shè)置一個listener用于監(jiān)聽綁定的對象的屬性變化。

public interface Observable {

    /**
     * Adds a callback to listen for changes to the Observable.
     * @param callback The callback to start listening.
     */
    void addOnPropertyChangedCallback(OnPropertyChangedCallback callback);

    /**
     * Removes a callback from those listening for changes.
     * @param callback The callback that should stop listening.
     */
    void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback);

    /**
     * The callback that is called by Observable when an observable property has changed.
     */
    abstract class OnPropertyChangedCallback {

        /**
         * Called by an Observable whenever an observable property changes.
         * @param sender The Observable that is changing.
         * @param propertyId The BR identifier of the property that has changed. The getter
         *                   for this property should be annotated with {@link Bindable}.
         */
        public abstract void onPropertyChanged(Observable sender, int propertyId);
    }
}

Observable接口有注冊/刪除監(jiān)聽的方法,但是數(shù)據(jù)變化時是否通知取決于開發(fā)者。為了簡化開發(fā),data binding提供了一個BaseObservable的基類,幫我們實現(xiàn)了監(jiān)聽的注冊和刪除。這個類也實現(xiàn)了通知數(shù)據(jù)變化的方法,在getter中使用Bindable注解,在setter中調(diào)用notifyPropertyChanged通知數(shù)據(jù)變更。

public class ObservableUser extends BaseObservable {
    public String firstName;
    public String lastName;

    public ObservableUser(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Bindable
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }

    @Bindable
    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
        notifyPropertyChanged(BR.lastName);
    }
}

Bindable注解會在編譯時在BR中生成一個entry,當數(shù)據(jù)變化時調(diào)用notifyPropertyChanged通知這個entry數(shù)據(jù)發(fā)生了變化。

ObservableFields

創(chuàng)建Observable類還是比較麻煩的,data binding為我們提供了一個便捷的ObservableField類以及它的派生類:
ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.

ObservableFields是包含了一個單一屬性的observable objects,可以通過聲明一個public final field來使用它:

public class ObservableFieldUser extends BaseObservable {
    public final ObservableField<String> firstName = new ObservableField<>();
    public final ObservableField<String> lastName = new ObservableField<>();

    public ObservableFieldUser(String firstName, String lastName) {
        this.firstName.set(firstName);
        this.lastName.set(lastName);
    }
}

然后可以用set/get來存取數(shù)據(jù):

user.firstName.set("bai");
String s = user.firstName.get();

Observable Collections

有些應(yīng)用希望使用更加靈活的結(jié)構(gòu)來管理數(shù)據(jù),Observable集合類允許使用key來訪問這些數(shù)據(jù)對象。

  • 如果key是String,ObservableArrayMap會非常有用:
ObservableMap<String, String> userMap = new ObservableArrayMap<>();
userMap.put("firstName", "bai");
userMap.put("lastName", "li");
binding.setUserMap(userMap);

然后在布局文件中用String keys獲取map中的數(shù)據(jù):

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="android.databinding.ObservableMap" />
        <variable
            name="userMap"
            type="ObservableMap<String,String>" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`obserable map : ` + userMap[`firstName`] + ` ` + userMap[`lastName`]}" />
    </LinearLayout>
</layout>
  • 如果key是integer,ObservableArrayList會非常有用:
ObservableList<User> useList = new ObservableArrayList<>();
useList.add(new User("bai", "li"));
binding.setUserList(useList);

然后在布局文件中使用下標獲取list中的數(shù)據(jù):

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="com.dragonjiang.databindingdemo.model.User" />
        <import type="android.databinding.ObservableList" />
        <variable
            name="userList"
            type="ObservableList<User>" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`obserable list : ` + userList[0].toString()}" />
    </LinearLayout>
</layout>

生成綁定

自動生成的Binding類都繼承了ViewDataBinding類,它們是連接layout的variables和Views的橋梁。

Creating

binding在View inflate之后創(chuàng)建。inflate方法會將Veiw綁定到binding上,對于不同的Veiw有不同的創(chuàng)建方法:

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);

如果布局使用不同的機制inflate,可以單獨綁定:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有時候綁定不能提前確定,例如ListView的Item layout,這時候可以使用DataBindingUtil類:

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

有ID的View

我們在之前的例子里面都沒有給View聲明一個id,因為用不到。但是如果有些情況下,我要調(diào)用到布局里面的特定的View,還是需要一個id。data binding提供了一個比findViewById更快的機制:

<TextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName + ` ` + user.lastName}" />

data binding會在binding類中自動生成對應(yīng)的屬性:

public final TextView tvName;

可以直接使用:

binding.tvName.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(LayoutDetailsActivity.this, binding.tvName.getText(), Toast.LENGTH_SHORT).show();
    }
});

ViewStubs

ViewStub不同于正常的View,它一開始是不可見的,在需要時才加載出特定的布局。所以data binding提供了一個ViewStubProxy類來代替ViewStub,開發(fā)者可以通過這個類來操作ViewStub。

ViewStub需要在inflate時候創(chuàng)建一個binding,故需要設(shè)置監(jiān)聽ViewStub.OnInflateLister

public class ViewStubActivity extends AppCompatActivity {

    private ActivityViewStubBinding mBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
        mBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub stub, View inflated) {
                IncludeBinding binding = DataBindingUtil.bind(inflated);
                binding.setUser(new User("bai", "li"));
            }
        });
    }

    public void onClick(View view) {
        if (!mBinding.viewStub.isInflated()) {
            mBinding.viewStub.getViewStub().inflate();
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data></data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="onClick"
            android:text="inflate view_stub" />

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

高級綁定

有些情況下,例如RecyclerView.Adapter中我們無法事先知道binding類。需要在onBindViewHodler(VH, int)中給binding賦值。
在這種情況下,RecyclerView布局內(nèi)都設(shè)置了一個item變量,可以通過getBinding方法返回一個ViewDataBinding類:

public void onBindViewHolder(VH holder, int position) {
    holder.binding.setModel(mDataList.get(position));
    holder.binding.executePendingBindings();
}

注意到上面executePendingBindings()表示立即綁定。如果沒有指定立即執(zhí)行,在數(shù)據(jù)變化時,binding會在下一幀開始前觸發(fā)。

屬性設(shè)置

當綁定的數(shù)據(jù)變化時,自動生成的binding類會尋找對應(yīng)屬性的setter方法。data binding框架設(shè)置了幾種自定義賦值的機制。

自動Setter

對于一個屬性,data binding 嘗試找到對應(yīng)的setter方法,例如我們自定義了一個UserView類,實現(xiàn)一個setUser方法:

public class UserView extends AppCompatTextView {
    public UserView(Context context) {
        super(context);
    }

    public UserView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public UserView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setUser(User user) {
        this.setText(user.toString());
    }
}

在布局文件中使用:

<com.dragonjiang.databindingdemo.ui.UserView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:user="@{user}" />

data binding自動為我們找到了setUser(User user)的方法。

重命名Setter

有的屬性的名稱與它的setter不匹配,對于這類屬性,可以使用注解BindingMethods將屬性與setter關(guān)聯(lián)起來。例如下面這個例子將andorid:tintsetImageTintList關(guān)聯(lián)起來:

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

自定義Setter(Binding Adapter)

有些屬性需要自定義屬性設(shè)置邏輯,例如沒有android:paddingLeft屬性對應(yīng)的setter方法。但是有setPadding(left, top, right, bottom)。一個用BindingAdapter注解的靜態(tài)方法允許開發(fā)者自定義setter:

@android.databinding.BindingAdapter("android:paddingLeft")
    public static void setPaddingLeft(View view, int padding) {
        view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom());
    }

BindingAdapter的方法還可以獲取舊的值。只需將舊的值放前面,新的值放后面:

@android.databinding.BindingAdapter("android:paddingLeft")
    public static void setPaddingLeft(View view, int oldPadding, int padding) {
        if (oldPadding != padding) {
            view.setPadding(padding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom());
        }
    }

BindingAdapter很強大,尤其對自定義屬性。比如可以用來異步加載圖片:

@android.databinding.BindingAdapter({"imageUrl", "error"})
    public static void loadImage(ImageView view, String url, Drawable error) {
        Glide.with(view.getContext()).load(url).error(error).into(view);
    }
<ImageView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:error="@{@drawable/ic_launcher}"
    app:imageUrl="@{user.avatar}" />

imageUrlerror屬性被使用時,就會匹配調(diào)用BindindAdapter的loadImage方法。

轉(zhuǎn)換器

對象轉(zhuǎn)換

如果binding表達式返回一個對象,data binding會尋找對應(yīng)的setter(自動setter、重命名setter、自定義setter),然后將返回的對象強制轉(zhuǎn)換成setter需要的類型。
這是一個使用ObservableMap的例子:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

userMap返回一個對象,這個對象會被自動轉(zhuǎn)換為setText(CharSequence)需要的類型。如果類型轉(zhuǎn)換有問題,開發(fā)者需要受到進行類型轉(zhuǎn)換。

自定義轉(zhuǎn)換

有時候需要對一些特定的類型直接做轉(zhuǎn)換,例如設(shè)置背景:

<com.dragonjiang.databindingdemo.ui.UserView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@{user.isAdult ? @color/colorAccent : @color/colorPrimary}"
    app:user="@{user}" />

這里background需要Drawable類型,而color是int類型,此時需要一個BindingConversation將int轉(zhuǎn)為ColorDrawable:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

注意:轉(zhuǎn)換只能在setter時生效,所以不允許混合類型

<View
   <!--這是不允許的-->
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容