MVVM之DataBinding學習筆記

前言

...想了半分鐘,好像并沒有啥需要特別聲明的,能翻到這篇文章的人,相信也早已了解了DataBinding是個什么東西,所以還是簡單粗暴點兒,就當給自己留個筆記...開擼吧!

配置

  • 確保sdk的support包更新到了最新版
  • 在對應module的build.gradle文件中進行如下配置(需AS版本1.5以上)
    android {
      ...
      dataBinding {
          enabled = true
      }
    }

    基本使用

    數據綁定

    Activity

    先上layout代碼
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android=";

<!--如果本頁不需要綁定數據,data標簽可以省略-->
<data class="DataBingMain">
    <import type="lxf.widget.util.AppUtils"/>
    <variable
        name="user"
        type="lxf.androiddemos.model.UserEntity"/>
</data>

<LinearLayout
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="lxf.androiddemos.ui.DatabindingActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:background="@mipmap/bg_robot"
    android:orientation="vertical"
    >
    <TextView
        style="@style/text_base"
        android:text="@{`name:`+user.name}"/>
     <TextView
        style="@style/text_base"
        android:text="@{user.sex}"/>
    <TextView
        style="@style/text_base"
        android:text="@{String.valueOf(user.age)}"/>
    <TextView
        style="@style/text_base"
        android:text="@{user.initType(user.type)}"/>
</LinearLayout>

</layout></code></pre>

  1. layout的編寫方式改變:新增layout和data標簽,layout為根布局,包含data和ui布局兩部分。data即要綁定的數據model,ui布局即我們以前寫法中的根布局。
  2. data標簽:
    • 使用DataBinding編寫布局,系統會自動生成一個繼承ViewDataBinding類,而class屬性可以指定這個類的名字,如果不指定,則會根據xml的名字自動生成。
    • variable可以設置多個。
    • 通俗的講,name即變量名,可以在下面直接引用,同時會在自動生成的ViewDataBinding類中自動生成setXXX和getXXX方法,用來綁定數據。
    • type即我們的數據model。
    • 支持import,導入后可以在@{}中直接使用,方式同java。
  3. @{}語法中支持大部分的java操作,當然最好不要寫太復雜的語句,如果有這個需求,可以在java類中寫一個方法,然后在此調用:
    • 運算符: + - / * % () && || & | ^ >> >>> << == > < >= <=
    • 字符串拼接 + (注意字符串要用``括起來,esc下面那個鍵)
    • instanceof
    • 方法調用
    • res資源訪問
    • 數組訪問 []
    • 三目運算 表達式1 ? 表達式2 : 表達式3
    • 聚合判斷 表達式1 ?? 表達式2 (表達式1為null,則返回表達式2)
    • 等等...

activity代碼

通過DataBindingUtil.setContentView方法代替原來的setContentView。

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    //1.獲取ViewDataBinding對象
    DataBingMain dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_databinding);

    //2.獲取數據
    UserEntity user = new UserEntity();
    user.setName("lxf");
    user.setSex("man");
    user.setAge(25);
    user.setType(1);

    //3.綁定數據
    dataBinding.setUser(user);
    //dataBinding.setVariable(BR.user,user);
}</code></pre> 

model代碼

package lxf.androiddemos.model;

public class UserEntity {
    private String name;
    private String sex;
    private int age;
    private int type;

    public String initType(int type){
        String result;
        switch (type){
            case 1:
                result = "程序猿";
                break;
            case 2:
                result = "程序猿的天敵";
                break;
            default:
                result = "無業游民";
                break;
        }
        return result;
    }

    //setter   getter方法略
}

activity和model代碼很簡單,就不需要解釋了。

Fragment

看到這里應該有個疑問:fragment中沒有setContentView方法,該怎么辦?

所幸DataBinding庫還提供了另外一個初始化布局的方法:DataBindingUtil.inflate()。

@Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        ViewDataBinding binding = DataBindingUtil.inflate(inflater,R.layout.fragment_blank,container,false);
        return binding.getRoot();
    }

xml布局的寫法同activity。

列表綁定

在此已RecyclerView為例。

單布局

先看item布局:

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

    <data>
        <variable
            name="item"
            type="lxf.androiddemos.model.MainRecyclerItem"/>
    </data>

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:layout_marginTop="16dp"
        app:cardElevation="5dp"
        android:onClick="@{item.onItemClick}"
        >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textSize="18sp"
            android:textColor="@color/text_green_bg"
            android:text="@{item.content}"/>
    </android.support.v7.widget.CardView>
</layout>

這里值得我們關注的有兩個地方,一個就是TextView上的數據綁定,一個是父布局上的onClick屬性,可以通過這種方式來設置item點擊事件,說白了其實就是調用MainRecyclerItem中的一個方法,我們可以通過getter方法很方便的知道當前item的具體數據,具體實現請往下看。

model實體類

public class MainRecyclerItem {
    public static final String[] items = new String[]{"ViewDragHelper", "自定義Behavior", "二維碼", "DataBinding"};

    private String content;

    public void onItemClick(View view) {
        Intent intent = null;
        switch (getContent()) {
            case "ViewDragHelper":
                intent = new Intent(view.getContext(), ViewDragHelperActivity.class);
                break;
            case "自定義Behavior":
                intent = new Intent(view.getContext(), BehaviorActivity.class);
                break;
            case "二維碼":
                intent = new Intent(view.getContext(), ZxingActivity.class);
                break;
            case "DataBinding":
                intent = new Intent(view.getContext(), DatabindingActivity.class);
                break;
        }
        if (intent != null)
            view.getContext().startActivity(intent);
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

adapter

class RecyclerBindingViewHolder extends RecyclerView.ViewHolder {
    ViewDataBinding binding;

    private RecyclerBindingViewHolder(ViewDataBinding binding) {
        super(binding.getRoot());
        this.binding = binding;
    }

    static RecyclerBindingViewHolder createViewHolder(ViewGroup parent, int layoutId) {
        ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),layoutId,parent,false);
        return new RecyclerBindingViewHolder(binding);
    }


public abstract class BaseRecyclerBindingAdapter extends RecyclerView.Adapter<RecyclerBindingViewHolder> implements ChangeDataLinstener{
    protected List<Object> mData;

    public BaseRecyclerBindingAdapter(List<Object> list) {
        mData = (list != null) ? list : new ArrayList<>();
    }

    @Override
    public RecyclerBindingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return RecyclerBindingViewHolder.createViewHolder(parent,getItemLayoutId(viewType));
    }

    @Override
    public void onBindViewHolder(RecyclerBindingViewHolder holder, int position) {
        //綁定數據
        holder.binding.setVariable(getItemVariableId(),mData.get(position));
        holder.binding.executePendingBindings();
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    public void setmData(List<Object> mData) {
        this.mData = mData;
        notifyDataSetChanged();
    }

    //item布局id
    public abstract int getItemLayoutId(int viewType);

    //對應item布局里面data標簽中的name,會自動生成一個BR.xxx屬性,類似于R文件
    public abstract int getItemVariableId();

}

我們這里把adapter寫成了一個抽象類,如果沒有什么很奇葩的要求,可以算一個通用adapter了,可以看到它沒有任何的findviewbyid和set數據,一切都在布局中封裝好了,實現非常的簡潔。

多布局

如果我們想加一個頭部文件,可以這樣:

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

    <data>
        <variable
            name="item"
            type="lxf.androiddemos.model.MainRecyclerHeader"/>
    </data>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:gravity="center"
        android:text="@{item.header}"
        android:background="@color/text_orange_bg">

    </TextView>
</layout>

注意和之前的item布局有個相通之處,就是data標簽的name屬性值是一樣的,然后通過getItemViewType實現我們的不同布局即可。

datas = new ArrayList<>();
MainRecyclerHeader header = new MainRecyclerHeader();
header.setHeader("我是頭部文件");
datas.add(header);
for (int i = 0; i < MainRecyclerItem.items.length; i++) {
    MainRecyclerItem item = new MainRecyclerItem();
    item.setContent(MainRecyclerItem.items[i]);
    datas.add(item);
}


BaseRecyclerBindingAdapter bindingAdapter = new BaseRecyclerBindingAdapter(datas) {
            @Override
            public int getItemLayoutId(int viewType) {
                return viewType;
            }

            @Override
            public int getItemVariableId() {
                return BR.item;//對應item布局里面data標簽中的name
            }

            @Override
            public int getItemViewType(int position) {
                if (position == 0)
                    return R.layout.header_recycler_main;
                else
                    return R.layout.item_recycler_main;
            }
        };

事件綁定

事件綁定說白了,其實就是一種特殊的變量綁定,或者說是一個方法的調用。

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

    <!--如果本頁不需要綁定數據,data標簽可以省略-->
    <data class="DataBingMain">
        <variable
            name="user"
            type="lxf.androiddemos.model.UserEntity"/>
        <variable
            name="util"
            type="lxf.androiddemos.test.TestUtil"/>
    </data>

    <LinearLayout
        ...
        >
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{util.onBtnClick}"
            />
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onTextChanged="@{util.onTextChanged}"
            />
    </LinearLayout>
</layout>


public class TestUtil {

    public void onBtnClick(View view){
        Toast.makeText(view.getContext(),"onBtnClick",Toast.LENGTH_SHORT).show();
    }
    public void onTextChanged(CharSequence s, int start, int before, int count){
        System.out.println(s);
    }
}

需要注意的是通過onclick這種方式綁定的事件,實現方法中一定要傳入view參數(類似于傳統的onClick方法),否則編譯會報錯。

同樣的官方文檔提到,你也可以用這種方式來綁定一些比較偏門的監聽,比如上面的onTextChanged,方法參數必須與傳統的onTextChanged參數一模一樣,否則編譯報錯,這種方式可以使你只監聽onTextChanged一個方法,而非TextWatcher的三個方法,另外EditText本身是沒有android:onTextChanged這個屬性的,具體實現原理需要先理解一下什么是databinding的自定義屬性,會在后文提到。

進階使用

數據更新

在很多情況下,我們需要動態去設置相關數據,DataBinding為我們提供了兩種方式來實現它。

方法一

  1. 實體類繼承BaseObservable,或者自己實現Observable
  2. 在需要刷新的屬性的get方法上添加@Bindable注解,此時會自動生成BR類。(這里有個坑,很多時候BR文件不會自動生成,此時需要重啟AS...請讓我先默默地日一波dog)
  3. 在相應的set方法里調用notifyPropertyChanged(BR.xxx)進行刷新。
package lxf.androiddemos.model;

import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.view.View;

import lxf.androiddemos.BR;

public class UserEntity extends BaseObservable{
    private String name;
    private String sex;
    private int age;
    private int type;

    ...

    public void addAge(View view) {
        setAge(getAge() + 1);
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
//        notifyChange();//刷新所有可刷新數據
        notifyPropertyChanged(BR.age);
    }
}

方法二

  1. 實體類繼承BaseObservable,或者自己實現Observable
  2. 使用ObservableField<>,泛型可以填入自己需要的類型,注意必須要初始化。對于基本數據類型也可以直接使用ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble和ObservableParcelable。
  3. 通過set和get方法為ObservableField設值和取值
package lxf.androiddemos.model;

import android.databinding.BaseObservable;
import android.databinding.ObservableField;
import android.view.View;

public class UserEntity extends BaseObservable{

    public ObservableField<String> address = new ObservableField<>();

    public void changeAddress(View view){
        address.set("change:" + address.get());
    }
}

自定義屬性綁定適配器和回調

自動尋找setter

DataBinding在遇到屬性綁定時,會自動去尋找該屬性的set方法,找到就會調用,找不到就報錯。

<ImageView
    android:layout_width="50dp"
    android:layout_height="50dp"
    app:imageResource="@{R.mipmap.ic_launcher}"/>

比如上面這段代碼,我們知道ImageView中是沒有imageResource這個屬性的,但是有setImageResource(int resId)方法,因此這段代碼是可以正常運行的。利用這種特性,可以為一些自定義控件增加setter方法,使其支持DataBinding。

@BindingMethods

當xml屬性名稱和源碼中set方法名稱不一致時,可以通過這種方式來進行綁定。先看一個官方的實現:

@BindingMethods({
        ...
        @BindingMethod(type = TextView.class, attribute = "android:inputType", method = "setRawInputType"),
        ...
})

這段代碼的意思就是將TextView的android:inputType屬性綁定到setRawInputType方法,其實也可以通俗的認為是為原本的setter方法起了一個別名。

@BindingAdapter

很多時候,源碼中并沒有提供set方法,比如ImageView,我們希望通過設置url來達到加載圖片的目的,我們可以通過@BindAdapter來實現。

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

    <!--如果本頁不需要綁定數據,data標簽可以省略-->
    <data class="DataBingMain">
        <import type="lxf.androiddemos.R"/>
        <variable
            name="user"
            type="lxf.androiddemos.model.UserEntity"/>
    </data>

    <LinearLayout
        ...
        >

        <ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:url="@{user.img}"
            app:placeHolder="@{R.mipmap.ic_launcher}"/>
    </LinearLayout>
</layout>


public class UserEntity extends BaseObservable{

    private String img;

    @BindingAdapter(value = {"url","placeHolder"},requireAll = false)
    public static void setImage(ImageView imageView ,String url,int placeHolder){
        Glide.with(imageView.getContext()).load(url).placeholder(placeHolder).into(imageView);
    }
}

這里有幾點需要注意:

  • xml文件中一定不要忘記各種類的import(除java.lang包外均需導入),否則你一定會碰到databinding程序包不存在這個錯誤。
  • 設置@BindAdapter注解的方法需要是static的,否則編譯也會報錯。
  • 你可以把這個方法設置在一個專門的工具類中,不是說必須要在這個model實體類里。
  • @BindAdapter包含value和requireAll兩個屬性,value是一個String[],包含你自定義的屬性。requireAll意思是是否需要設置你在value中聲明的全部屬性,默認為true。如果設定為false,那么沒賦值的自定義屬性會傳默認值。

到這里,我們來回頭看一下之前在 事件綁定 中留下的那個坑——onTextChanged,其實這是官方提前為我們封裝好的:

@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
            "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
    public static void setTextWatcher(TextView view, final BeforeTextChanged before,
            final OnTextChanged on, final AfterTextChanged after,
            final InverseBindingListener textAttrChanged) {
        final TextWatcher newValue;
        if (before == null && after == null && on == null && textAttrChanged == null) {
            newValue = null;
        } else {
            newValue = new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    if (before != null) {
                        before.beforeTextChanged(s, start, count, after);
                    }
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (on != null) {
                        on.onTextChanged(s, start, before, count);
                    }
                    if (textAttrChanged != null) {
                        textAttrChanged.onChange();
                    }
                }

                @Override
                public void afterTextChanged(Editable s) {
                    if (after != null) {
                        after.afterTextChanged(s);
                    }
                }
            };
        }
        final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
        if (oldValue != null) {
            view.removeTextChangedListener(oldValue);
        }
        if (newValue != null) {
            view.addTextChangedListener(newValue);
        }
    }

可以看到,當on!=null時,會調用傳統的onTextChanged方法。

@BindingConversion

方法注釋,當自定義的屬性和setter方法中需要的參數類型不符時進行轉換。

<Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@{`#de325e`}"
            />

上面這種寫法,直接編譯是會報錯的,因為setBackground接收的是一個Drawable對象,而我們傳入的是個string,所以我們此處可以用@BindingConversion來轉換一下(PS:我知道傳統寫法是可以直接傳字符串顏色值的,我只是舉個簡單例子)。

@BindingConversion
    public static Drawable colorToDrawable(String color){
        return new ColorDrawable(Color.parseColor(color));
    }

DataBinding在碰到這種參數類型不對的問題時,會自動去檢索看看有沒有相關的@BindingConversion方法,如果有的話則會調用,需要注意,這個方法也需要是static的。

接口回調

model的回調

當屬性值變化時的回調。

user.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
            @Override
            public void onPropertyChanged(Observable sender, int propertyId) {
                if (propertyId == BR.age){
                    Toast.makeText(getApplicationContext(),"age刷新了",Toast.LENGTH_SHORT).show();
                }
            }
        });

ViewDatabinding的回調

當ViewDataBinding執行executePendingBindings()尺寸必須再次評估時的回調。可以設置一些view的展示動畫等。

dataBinding.addOnRebindCallback(new OnRebindCallback() {
            @Override
            public boolean onPreBind(ViewDataBinding binding) {
                return super.onPreBind(binding);
            }

            @Override
            public void onCanceled(ViewDataBinding binding) {
                super.onCanceled(binding);
            }

            @Override
            public void onBound(ViewDataBinding binding) {
                super.onBound(binding);
            }
        });

雙向綁定

基本數據

雙向綁定意思不僅數據綁定UI,同時UI更新時可以刷新數據,語法為@={},舉個例子:

<EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onTextChanged="@{util.onTextChanged}"
            android:text="@={user.address}"
            />

這樣在程序運行后,editText會自動顯示user.address的初始值,改變editText,則user.address也會同步改變,可以想象,如果我們將user.address綁定另一個TextView,則TextView的內容會跟隨editText的變化而變化。

隱式屬性監聽

<ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:visibility="@{checkbox.checked?View.VISIBLE:View.GONE}"
            app:placeHolder="@{R.mipmap.ic_launcher}"
            app:url="@{user.img}" />
        <CheckBox
            android:id="@+id/checkbox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

可以看ImageView的visibility屬性,通過CheckBox的checked屬性來控制自身的顯示和隱藏,這是官方給出的一種支持,同時官方還支持下面這些屬性:

  • AbsListView android:selectedItemPosition
  • CalendarView android:date
  • CompoundButton android:checked
  • DatePicker android:year, android:month, android:day (yes, these are synthetic, but we had a listener, so we thought you’d want to use them)
  • NumberPicker android:value
  • RadioGroup android:checkedButton
  • RatingBar android:rating
  • SeekBar android:progress
  • TabHost android:currentTab (you probably don’t care, but we had the listener)
  • TextView android:text
  • TimePicker android:hour, android:minute (again, synthetic, but we had the listener)

自定義雙向綁定

雙向綁定其實就是正向綁定+反向綁定,前面講的全部是正向綁定,截下來我們來看看怎么定義反向綁定。

綁定方法(@InverseBindingMethods)

首先先來了解幾個名詞:

  • @InverseBindingMethods:其實就是元素為@InverseBindingMethod的一個數組,用來注解
    @Target(ElementType.TYPE)
    public @interface InverseBindingMethods {
      InverseBindingMethod[] value();
    }
  • @InverseBindingMethod:反向綁定方法,用來確定怎么去監聽view屬性的變化和回調哪一個getter方法。包含以下4個屬性:

    • type:包含attribute的view類型。
    • attribute:支持雙向綁定的屬性(string格式)。
    • event:可以省略,用來通知DataBinding系統attribute已經改變,默認為attribute + "AttrChanged"。(UI通知數據)
    • method:可以省略,用來從view獲取數據的方法,不設定的話會自動尋找"is" 或 "get" + attribute方法。(數據刷新)

event調用時機需要通過@BindingAdapter進行設置。

  • InverseBindingListener:反向綁定監聽器,當使用雙向綁定時,會在你的layout自動生成的binding類中自動生成一個InverseBindingListener的實現(拗口嗎?好像有一點點。。不理解的可以去看看源碼)。

看完這幾個名詞是不是已經凌亂了?(話說我當時也差點哭了。。),我們來看個官方例子消化一下:

@InverseBindingMethods({
        @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),
})//1.這里需要雙向綁定的是checked屬性,event和method都省略了。
public class CompoundButtonBindingAdapter {
    ...
    //2.設置什么時候調用event
    @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();
                }
            });
        }
    }
}

    //3.我們在layout中使用雙向綁定
        <CheckBox
            android:id="@+id/checkbox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:checked="@={user.checked}"/>

    //4.layout的binding類中自動生成的InverseBindingListener實現。
    // Inverse Binding Event Handlers
    private android.databinding.InverseBindingListener checkboxandroidCheck = new android.databinding.InverseBindingListener() {
        @Override
        public void onChange() {//這段邏輯其實就是用來更新user實體類中的checked字段的
            // Inverse of user.checked.get()
            //         is user.checked.set((java.lang.Boolean) callbackArg_0)
            boolean callbackArg_0 = checkbox.isChecked();//其實就是method
            // localize variables for thread safety
            // user.checked != null
            boolean checkedUserObjectnul = false;
            // user.checked
            android.databinding.ObservableField<java.lang.Boolean> checkedUser = null;
            // user
            lxf.androiddemos.model.UserEntity user = mUser;
            // user.checked.get()
            java.lang.Boolean CheckedUser1 = null;
            // user != null
            boolean userObjectnull = false;

            userObjectnull = (user) != (null);
            if (userObjectnull) {
              checkedUser = user.checked;

                checkedUserObjectnul = (checkedUser) != (null);
                if (checkedUserObjectnul) {
                checkedUser.set((java.lang.Boolean) (callbackArg_0));
                }
            }
        }
    };

整個反向綁定的流程下來其實就是:

  1. 定義需要反向綁定的屬性(checked),并配置event(checkedAttrChanged)和method(isChecked)。
  2. 系統會自動根據event找到對應的方法(setLinstener),配置好調用時機。
  3. 開發者在layout中使用雙向綁定。
  4. 自動在binding類中生成一個InverseBindingListener的實現。

    綁定適配器(@InverseBindingAdapter)

    下面再來看個新名詞...( ╯□╰ ):
  5. @InverseBindingAdapter:反向綁定適配器,用來注解 方法 。只包含attribute和event兩個屬性,含義同上:
    • attribute:支持雙向綁定的屬性(string格式)。
    • event:可以省略,用來通知DataBinding系統attribute已經改變,默認為attribute + "AttrChanged"。需要通過@BindingAdapter進行設置調用時機。

@InverseBindingAdapter注解的方法本身就相當于獲取數據的getter方法(類似于@BindingAdapter注解的方法本身就相當于setter方法)。

官方案例(雙向綁定android:text):

//1.這一步相當于做了兩個操作:確定綁定的屬性和event;指定getter方法
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
    public static String getTextString(TextView view) {
        return view.getText().toString();
    }
//2.根據event找到對應方法,配置event的調用時機。    
@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
            "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
    public static void setTextWatcher(TextView view, final BeforeTextChanged before,
            final OnTextChanged on, final AfterTextChanged after,
            final InverseBindingListener textAttrChanged) {
        final TextWatcher newValue;
        if (before == null && after == null && on == null && textAttrChanged == null) {
            newValue = null;
        } else {
            newValue = new TextWatcher() {
                ...
                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (on != null) {
                        on.onTextChanged(s, start, before, count);
                    }
                    if (textAttrChanged != null) {
                        textAttrChanged.onChange();
                    }
                }
                ....
            };
        }
       ...
    }
    //3.使用雙向綁定    
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onTextChanged="@{util.onTextChanged}"
            android:text="@={user.address}" />
    //4.binding類中自動生成InverseBindingListener的實現。
private android.databinding.InverseBindingListener mboundView10androidT = new android.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
            // Inverse of user.address.get()
            //         is user.address.set((java.lang.String) callbackArg_0)
            java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView10);//getter方法
            // localize variables for thread safety
            // user.address != null
            boolean addressUserObjectnul = false;
            // user
            lxf.androiddemos.model.UserEntity user = mUser;
            // user.address
            android.databinding.ObservableField<java.lang.String> addressUser = null;
            // user.address.get()
            java.lang.String AddressUser1 = null;
            // user != null
            boolean userObjectnull = false;

              userObjectnull = (user) != (null);
            if (userObjectnull) {
                addressUser = user.address;
                addressUserObjectnul = (addressUser) != (null);
                if (addressUserObjectnul) {
                     addressUser.set((java.lang.String) (callbackArg_0));
                }
            }
        }
    };

一大堆的代碼看下來,其實綁定方法和綁定適配器兩種方法的最終效果是一樣的,實現過程也是大同小異,這里就不贅述了,和上面的綁定方法基本一致。

比葫蘆畫瓢

我們來自定義實現這樣一個效果,點擊改變自定義view的顏色,同時將色值在另一個TextView中展示出來(雖然沒什么卵用,僅僅當個案例吧),效果圖如下(請自覺忽略其他的東西。。):

效果圖

實現過程:

//1.自定義ColorPicker,并為color屬性添加getter和setter方法
public class ColorPicker extends View {
    ...
    private String mColor;

    public String getColor() {
        return mColor;
    }

    public void setColor(String mColor) {
        this.mColor = mColor;
        paint.setColor(Color.parseColor(mColor));
        invalidate();
    }

    ...
}
//2.自定義反向綁定
@InverseBindingMethods({
        @InverseBindingMethod(type = ColorPicker.class,attribute = "color")
})
public class ColorPickerAdapter {

    @BindingAdapter(value = {"colorAttrChanged"},requireAll = false)
    public static void setListener(ColorPicker picker, final InverseBindingListener attrChange){
        if (attrChange!=null){
            picker.setOnColorChangeListener(new ColorPicker.OnColorChangeListener() {
                @Override
                public void onColorChange(ColorPicker picker, String color) {
                    //...

                    attrChange.onChange();
                }
            });
        }
    }
}
//3.在layout中使用雙向綁定
<lxf.androiddemos.test.ColorPicker
     android:layout_width="100dp"
     android:layout_height="100dp"
     app:color="@={user.color}" />

上面給出了關鍵代碼,剛接觸DataBinding的萌新如果理解不了可以去文末下載Demo看看,只是一個很簡單的案例,應該沒什么問題。

接下來我們用@InverseBindingAdapter來實現同樣的效果:

public class ColorPickerAdapter {

    @InverseBindingAdapter(attribute = "color")
    public static String getColor(ColorPicker picker){
        return picker.getColor();
    }

    @BindingAdapter(value = {"colorAttrChanged"},requireAll = false)
    public static void setListener(ColorPicker picker, final InverseBindingListener attrChange){
        if (attrChange!=null){
            picker.setOnColorChangeListener(new ColorPicker.OnColorChangeListener() {
                @Override
                public void onColorChange(ColorPicker picker, String color) {
                    //...

                    attrChange.onChange();
                }
            });
        }
    }
}

另外關于一些情況下雙向綁定存在的死循環問題,只要在setter方法中判斷一下新老值不同即可。

依賴注入

DataBindingComponent,一般用于一個@BindingAdapter方法需要有多種實現時(比如說測試。。),我們來看一下前面那個修改年齡age的例子:

//原來的方式
  @BindingAdapter(value = {"url","placeHolder"},requireAll = false)
    public static void setImage(ImageView imageView , String url, int placeHolder){
        ImgLoadUtil.load(imageView,url,placeHolder);
    }

//運用DataBindingComponent
//1.如果需要多種實現,可以先建一個抽象的adapter,注意方法為非靜態的
public abstract class AppAdapter {

    @BindingAdapter(value = {"url","placeHolder"},requireAll = false)
    public abstract void setImage(ImageView imageView , String url, int placeHolder);
}
//2.添加抽象adapter的實現,這里我們只寫了一個
public class ImgAdapter extends AppAdapter {
    @Override
    public void setImage(ImageView imageView, String url, int placeHolder) {
        ImgLoadUtil.load(imageView,url,placeHolder);
    }
}
public class Img2Adapter extends AppAdapter {
    @Override
    public void setImage(ImageView imageView, String url, int placeHolder) {
        ...
    }
}
//3.添加DataBindingComponent的實現(非靜態的@BindingAdapter注解方法會自動在DataBindingComponent中生成相應的getter方法)。
public class MyComponent implements android.databinding.DataBindingComponent {
    @Override
    public AppAdapter getAppAdapter() {
        return new ImgAdapter();
    }
}
public class My2Component implements android.databinding.DataBindingComponent {
    @Override
    public AppAdapter getAppAdapter() {
        return new Img2Adapter();
    }
}
//4.Activity中調用
        //DataBingMain dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_databinding);
        //DataBindingUtil.setDefaultComponent(new MyComponent());
        DataBingMain dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_databinding,new MyComponent());

最終效果是一模一樣的。

遇到的坑

  • Error:(8, 36) 錯誤: 程序包lxf.androiddemos.databinding不存在

    遇到這種情況一般都是xml中的問題,比如data標簽中引入的包名不對,或者是布局里面使用了什么錯誤的屬性,等等。。。數據量大的時候,這種錯誤一般比較難找,簡直就是日了dog。

  • 需要更新數據時,為getter方法設置@Bindable,很多時候BR文件不會生成,需要重啟AS,默默地再日一波dog。

  • 最好不要使用clean project,否則R文件和BR文件會被清掉,R文件會自動重新生成,至于BR文件...那只dog,麻煩你再過來一下。

 

 

來自:http://www.jianshu.com/p/05b9838a1949

 

 本文由用戶 hkxf9949 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!