Android Data Binding——高级
上一篇文章Android Data Binding——进阶
介绍了Data Binding的语法等进阶功能。这一篇我们来介绍一下Data Binding的数据对象。
文中的例子可前往DataBindingDemo查看。
任何POJO对象都可以用在data binding中,但是对象改变时候,要如何通知UI更新呢?这是使用Data Binding最奥妙的地方。Ps:我们这边只是介绍如何使用,没有涉及到实现原理。
有三种不同的数据变化通知机制:observable objects
, observable fields
, and observable collections
.
这些observable对象绑定到UI上,当对象的属性更改时就会自动通知UI更新。
Observale Objects
一个继承Observable
接口的类,data binding会设置一个listener用于监听绑定的对象的属性变化。
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
接口有注册/删除监听的方法,但是数据变化时是否通知取决于开发者。为了简化开发,data binding提供了一个BaseObservable
的基类,帮我们实现了监听的注册和删除。这个类也实现了通知数据变化的方法,在getter
中使用Bindable
注解,在setter
中调用notifyPropertyChanged
通知数据变更。
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,当数据变化时调用notifyPropertyChanged
通知这个entry数据发生了变化。
ObservableFields
创建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
来存取数据:
user.firstName.set("bai");
String s = user.firstName.get();
Observable Collections
有些应用希望使用更加灵活的结构来管理数据,Observable集合类允许使用key来访问这些数据对象。
- 如果key是String,
ObservableArrayMap
会非常有用:
ObservableMap<String, String> userMap = new ObservableArrayMap<>();
userMap.put("firstName", "bai");
userMap.put("lastName", "li");
binding.setUserMap(userMap);
然后在布局文件中用String keys获取map中的数据:
<?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中的数据:
<?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之后创建。inflate方法会将Veiw绑定到binding上,对于不同的Veiw有不同的创建方法:
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,因为用不到。但是如果有些情况下,我要调用到布局里面的特定的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类中自动生成对应的属性:
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
,开发者可以通过这个类来操作ViewStub
。
ViewStub
需要在inflate时候创建一个binding,故需要设置监听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布局内都设置了一个item
变量,可以通过getBinding
方法返回一个ViewDataBinding
类:
public void onBindViewHolder(VH holder, int position) {
holder.binding.setModel(mDataList.get(position));
holder.binding.executePendingBindings();
}
注意到上面executePendingBindings()
表示立即绑定。如果没有指定立即执行,在数据变化时,binding会在下一帧开始前触发。
属性设置
当绑定的数据变化时,自动生成的binding类会寻找对应属性的setter方法。data binding框架设置了几种自定义赋值的机制。
自动Setter
对于一个属性,data binding 尝试找到对应的setter方法,例如我们自定义了一个UserView
类,实现一个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关联起来。例如下面这个例子将andorid:tint
与setImageTintList
关联起来:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
自定义Setter(Binding Adapter)
有些属性需要自定义属性设置逻辑,例如没有android:paddingLeft
属性对应的setter方法。但是有setPadding(left, top, right, bottom)
。一个用BindingAdapter
注解的静态方法允许开发者自定义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}" />
当imageUrl
和error
属性被使用时,就会匹配调用BindindAdapter的loadImage
方法。
转换器
对象转换
如果binding表达式返回一个对象,data binding会寻找对应的setter(自动setter、重命名setter、自定义setter),然后将返回的对象强制转换成setter需要的类型。
这是一个使用ObservableMap
的例子:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap
返回一个对象,这个对象会被自动转换为setText(CharSequence)
需要的类型。如果类型转换有问题,开发者需要受到进行类型转换。
自定义转换
有时候需要对一些特定的类型直接做转换,例如设置背景:
<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转为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"/>