Data Binding MVVM 数据绑定 总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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,那么自动生成的类就是 ActivityBasicBinding

创建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 parameter 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


















































































以上是关于Data Binding MVVM 数据绑定 总结的主要内容,如果未能解决你的问题,请参考以下文章

Data Binding 的使用之一:简单的数据绑定

Data Binding Guide——google官方文档翻译(上)

Android开发教程 - 使用Data Binding 介绍

data binding 优缺点

绑定:未找到属性。 MVVM

XAML数据绑定(Data Binding)