本文基于:
androidx.databinding:databinding-runtime:3.4.1
一.Data-Binding简介
数据绑定库是一种支持库,借助该库,可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。详细的文档介绍参考:
https://developer.android.com/topic/libraries/data-binding
二.DataBinding原理
1. 示例
这个例子只是简单的将用户信息通过TextView显示,布局如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="user" type="com.xiaomi.zxm.databinding.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:orientation="horizontal"
tools:context=".databinding.DataBindingActivity1">
<TextView
android:id="@+id/btn_db_firstname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background="#FFD56B"
android:text="@{user.firstName}" />
<TextView
android:id="@+id/btn_db_lastname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background="#FFD56B"
android:text="@{user.lastName}" />
</LinearLayout>
</layout>
需要将布局文件使用layout包装起来,layout下面有两个tag,一个data,一个是真正的布局LinearLayout,可以看到我们在text属性上使用了一个表达式@{user.lastName},来指定其值是由前面定义的data绑定的。继续看其他的文件:
public class DataBindingActivity1 extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UserDataBinding binding =
DataBindingUtil.setContentView(this, R.layout.user_data);
User user = new User("zhang", "xinming");
binding.setUser(user);
}
}
public class User {
private final String mFirstName;
private final String mLastName;
public User(String firstName, String lastName) {
this.mFirstName = firstName;
this.mLastName = lastName;
}
public String getFirstName() {
return mFirstName;
}
public String getLastName() {
return mLastName;
}
}
在Activity中,通过DataBindingUtil.setContentView将布局设置进去,会返回一个UserDataBinding的对象,通过它的setUser就可以将User对象设置进去并展示到绑定的TextView上。如果不是在Activity中,比如在Fragment或者某些RecyclerView的适配器中使用数据绑定,也可以这样使用:
UserDataBinding binding =
UserDataBinding.inflate(getLayoutInflater(), R.layout.user_data, false);
UserDataBinding binding =
DataBindingUtil.inflate(getLayoutInflater(), R.layout.user_data, parent, false);
2. Data-Binding内部实现
2.1 Data-Binding类图
黄色的类是我们定义的User,绿色的是data-binding生成的类,注意
UserDataBinding的名称和我们的布局文件名有关:user_data.xml生成UserDataBinding和UserDataBindingImpl,路径如下:
./app/build/generated/source/apt/debug/com/xiaomi/zxm/databinding/UserDataBindingImpl.java
./app/build/generated/data_binding_base_class_source_out/debug/dataBindingGenBaseClassesDebug/out/com/xiaomi/zxm/databinding/UserDataBinding.java
其余的是data-binding的类库里的类。DataBindingUtil是整个入口,他会持有一个DataBinderMapper,mapper里记录了各个DataBinding,注意这里有三个类叫做DataBinderMapperImpl,直接继承DataBinderMapper的data-binding里的类,MergedDataBinderMapper里有一个Mapper的列表,androidx.databinding.DataBinderMapperImpl是aapt生成,其代码只有一个构造函数:
DataBinderMapperImpl() {
addMapper(new com.xiaomi.zxm.DataBinderMapperImpl());
}
其作用就是new一个另外的DataBinderMapperImpl(注意其包名),并添加到MergedDataBinderMapper的Mapper列表里,也就是说真正干活的是 com.xiaomi.zxm.DataBinderMapperImpl。
2.2 Data-Binding创建过程
程序的入口是setContentView,传入的布局文件是一个特殊的被layout包装的,实际上这个文件,在编译后会被拆分成2个:
./app/build/intermediates/incremental/mergeDebugResources/stripped.dir/layout/user_data.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:orientation="horizontal"
tools:context=".databinding.DataBindingActivity1"
android:tag="layout/user_data_0">
<TextView
android:id="@+id/btn_db_firstname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background="#FFD56B"
android:tag="binding_1" />
<TextView
android:id="@+id/btn_db_lastname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background="#FFD56B"
android:tag="binding_2" />
</LinearLayout>
这个是真正的布局文件,但是给每一个需要绑定的view都加了tag,根view是layout/user_data_0, 两个TextView上分别binding_1和binding_2。另一个是:
./app/build/intermediates/data_binding_layout_info_type_merge/debug/mergeDebugResources/out/user_data-layout.xml:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout absoluteFilePath="/home/zhangxinming/projects/AndroidStudio/MyDemo/app/src/main/res/layout/user_data.xml" directory="layout"
isMerge="false"
layout="user_data" modulePackage="com.xiaomi.zxm">
<Variables name="user" declared="true" type="com.xiaomi.zxm.databinding.User">
<location endLine="5" endOffset="69" startLine="5" startOffset="8" />
</Variables>
<Variables name="address" declared="true"
type="com.xiaomi.zxm.databinding.Address">
<location endLine="6" endOffset="75" startLine="6" startOffset="8" />
</Variables>
<Targets>
<Target tag="layout/user_data_0" view="LinearLayout">
<Expressions />
<location endLine="32" endOffset="18" startLine="9" startOffset="4" />
</Target>
<Target id="@+id/btn_db_firstname" tag="binding_1" view="TextView">
<Expressions>
<Expression attribute="android:text" text="user.firstName">
<Location endLine="22" endOffset="43" startLine="22" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="22" endOffset="41" startLine="22"
startOffset="28" />
</Expression>
</Expressions>
<location endLine="22" endOffset="46" startLine="16" startOffset="8" />
</Target>
<Target id="@+id/btn_db_lastname" tag="binding_2" view="TextView">
<Expressions>
<Expression attribute="android:text" text="user.lastName">
<Location endLine="30" endOffset="42" startLine="30" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="30" endOffset="40" startLine="30"
startOffset="28" />
</Expression>
</Expressions>
<location endLine="30" endOffset="45" startLine="24" startOffset="8" />
</Target>
</Targets>
</Layout>
该配置文件中详细的记述了我们在原始布局文件中定的data相关的信息,我们声明的全局变量,变量指向的数据类型的绝对路径。
<Target id="@+id/btn_db_firstname" tag="binding_1" view="TextView" > ,tag对应的View类型。
<Expression attribute="android:text" text="user.lastName">,控件绑定具体属性和Model中的具体属性。
<TwoWay>true</TwoWay>是否是双向绑定(android:text="@{user.lastName}"是单向绑定,android:text="@={user.lastName}"是双向绑定,多一个等号)
2.2.1 setContentView的过程
public static <T extends ViewDataBinding> T setContentView(
@NonNull Activity activity, int layoutId,
@Nullable DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView =
(ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
其实代码分成两块,前面的作用和普通的setContentView是一样的,最后一句才是data-binding相关的。
bindToAddedView最后调用到bind:
static <T extends ViewDataBinding> T bind(
DataBindingComponent bindingComponent, View root, int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
sMapper就是DataBinderMapper,这个是抽象类,具体实现是MergedDataBinderMapper:
public ViewDataBinding getDataBinder(
DataBindingComponent bindingComponent, View view, int layoutId) {
for(DataBinderMapper mapper : mMappers) {
ViewDataBinding result = mapper.getDataBinder(bindingComponent, view, layoutId);
if (result != null) {
return result;
}
}
if (loadFeatures()) {
return getDataBinder(bindingComponent, view, layoutId);
}
return null;
}
内部循环每一个Mapper去调用getDataBinder,注意之前的add:
DataBinderMapperImpl() {
addMapper(new com.xiaomi.zxm.DataBinderMapperImpl());
}
也就是说最后会调用com.xiaomi.zxm.DataBinderMapperImpl的getDataBinder:
public ViewDataBinding getDataBinder(
DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if(localizedLayoutId > 0) {
final Object tag = view.getTag();
if(tag == null) {
throw new RuntimeException("view must have a tag");
}
switch(localizedLayoutId) {
case LAYOUT_USERDATA: {
if ("layout/user_data_0".equals(tag)) {
return new UserDataBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for company_data is invalid.
Received: " + tag);
}
}
}
return null;
}
可以看到如果tag是layout/user_data_0就返回UserDataBindingImpl对象。
2.2.2 UserDataBindingImpl的构造过程
public UserDataBindingImpl(@Nullable androidx.databinding.DataBindingComponent
bindingComponent, @NonNull View root) {
this(bindingComponent, root,
mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));
}
private UserDataBindingImpl(androidx.databinding.DataBindingComponent
bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 0, (android.widget.TextView) bindings[1]
, (android.widget.TextView) bindings[2]);
this.btnDbFirstname.setTag(null);
this.btnDbLastname.setTag(null);
this.mboundView0 = (android.widget.LinearLayout) bindings[0];
this.mboundView0.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
在构造函数执行之前,会执行mappingBindings方法,它实际上就是替代了之前的findViewById的工作。mappingBindings的代码比较长,这里不再列出来,其最终会把所有的tag是以layout开始的或者binding_开始的view都找出来,存到bindings数组里,这个数组就是mappingBindings方法的返回值,在UserDataBindingImpl的构造函数中,调用super的时候,将bindings数组分开了:bindings[1],bindings[2]他们就是2个需要绑定的TextView,来看看其super做了什么:
protected UserDataBinding(Object _bindingComponent, View _root, int _localFieldCount,
TextView btnDbFirstname, TextView btnDbLastname) {
super(_bindingComponent, _root, _localFieldCount);
this.btnDbFirstname = btnDbFirstname;
this.btnDbLastname = btnDbLastname;
}
只是简单的将2个TextView放到成员变量里,和我们直接使用findViewById存下来其实完全一样。
构造函数的最后执行了setRootTag:
protected void setRootTag(View view) {
view.setTag(R.id.dataBinding, this);
}
在布局的rootview上设置tag,id是R.id.dataBinding,对象就是当前ViewDataBinding对象,回忆一下,rootview上还有一个tag,是在xml拆分的时候设置的,是没有key的版本,其value是layout/user_data_0。
2.3 执行绑定
总结一下上面的步骤:
- 首先在rootView上setTag为layout/user_data_0,在需要绑定的子view上设置setTag为bind_1,binding_2
- 执行mappingBindings方法,将前面设置tag的view以成员变量的方式存起来。
- 构造UserDataBindingImpl,并在rootview上设置key=R.id.dataBinding ,value=UserDataBindingImpl对象的tag。
有了前面的步骤,接下来就是最后一步了,执行具体的绑定。在ViewDataBinding中,有一段静态代码:
static {
if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
ROOT_REATTACHED_LISTENER = null;
} else {
ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
@TargetApi(VERSION_CODES.KITKAT)
@Override
public void onViewAttachedToWindow(View v) {
// execute the pending bindings.
final ViewDataBinding binding = getBinding(v);
binding.mRebindRunnable.run();
v.removeOnAttachStateChangeListener(this);
}
};
}
}
当rootview被attach后,就会执行mRebindRunnable,其中getBinding就是找rootview上的id是R.id.dataBinding的tag,接下来的mRebindRunnable,最终调用的是executePendingBindingsInternal,内部执行executeBindings方法,这个方法是抽象方法,具体实现在UserDataBindingImpl中:
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String userFirstName = null;
com.xiaomi.zxm.databinding.User user = mUser;
java.lang.String userLastName = null;
com.xiaomi.zxm.databinding.Address address = mAddress;
java.lang.String addressCountry = null;
java.lang.String addressCity = null;
if ((dirtyFlags & 0x5L) != 0) {
if (user != null) {
// read user.firstName
userFirstName = user.getFirstName();
// read user.lastName
userLastName = user.getLastName();
}
}
if ((dirtyFlags & 0x6L) != 0) {
if (address != null) {
// read address.country
addressCountry = address.getCountry();
// read address.city
addressCity = address.getCity();
}
}
// batch finished
if ((dirtyFlags & 0x6L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(
this.btnDbCity, addressCity);
androidx.databinding.adapters.TextViewBindingAdapter.setText(
this.btnDbCountry, addressCountry);
}
if ((dirtyFlags & 0x5L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(
this.btnDbFirstname, userFirstName);
androidx.databinding.adapters.TextViewBindingAdapter.setText(
this.btnDbLastname, userLastName);
}
}
dirtyFlag用来标识需要刷新的view,这里demo里我只绑定User,为了更清晰的说明这个flag的用途,在demo中再增加一个Address的数据进行绑定,最后其flag如下:
// dirty flag
private long mDirtyFlags = 0xffffffffffffffffL;
/* flag mapping
flag 0 (0x1L): user
flag 1 (0x2L): address
flag 2 (0x3L): null
flag mapping end*/
看注释其实已经很清楚了,flag=0表示刷新user相关的view,flag=1表示刷新address相关的view,flag=2表示刷新所有的view,查看源代码可以对应出flag值(注意dirtyFlag的值是2的flag次方):
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x4L;
}
requestRebind();
}
public void setUser(@Nullable com.xiaomi.zxm.databinding.User User) {
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
public void setAddress(@Nullable com.xiaomi.zxm.databinding.Address Address) {
this.mAddress = Address;
synchronized(this) {
mDirtyFlags |= 0x2L;
}
...
}
回到刚才的刷新部分,dirtyFlags & 0x5L != 0表示要么刷新所有要么刷新user,dirtyFlags & 0x6L != 0表示要么刷新所有要么刷新address,如果刷新user,先从mUser中取出数据,在调用setText进行绑定:
androidx.databinding.adapters.TextViewBindingAdapter.setText(
this.btnDbFirstname, userFirstName);
其第一个参数是TextView,第二个参数user中的字段。注意TextViewBindingAdapter是androidx.databinding.adapters包下的,这个包有很多类似的Adapter,例如:AbsListViewBindingAdapter,CardViewBindingAdapter等待,这里不再一一列出。
上面的setUser的实现中,最后两句代码:
notifyPropertyChanged(BR.user);
super.requestRebind();
notifyPropertyChanged会通知一些callback,requestRebind则会执行executeBindings重新刷新。先看下notifyPropertyChanged,实际上,在ViewDataBinding继承了BaseObservable,实现了观察者模式,在我们的Activity获取到想要的binding对象后,就可以设置callback:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UserDataBinding binding =
DataBindingUtil.setContentView(this, R.layout.user_data);
User user = new User("zhang", "xinming");
binding.setUser(user);
binding.addOnPropertyChangedCallback(...);
}
ViewDataBinding继续看requestRebind:
protected void requestRebind() {
if (mContainingBinding != null) {
mContainingBinding.requestRebind();
} else {
final LifecycleOwner owner = this.mLifecycleOwner;
if (owner != null) {
Lifecycle.State state = owner.getLifecycle().getCurrentState();
if (!state.isAtLeast(Lifecycle.State.STARTED)) {
return; // wait until lifecycle owner is started
}
}
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}
先忽略mContainingBinding的分支,代码中先判断是否有LifecycleOwner,如果有,则必现至少在STARTED状态(与lifecycle的关系,binding上可以设置setLifecycleOwner参考后面的绑定LiveData)接下来就是判断当前是否还在mPendingRebind,USE_CHOREOGRAPHER = SDK_INT >= 16,当sdk大于等于16时,使用Choreographer来调度,FrameCallback可以在下一个frame被渲染的时候被回调,否则直接在主线程post。不管怎么样,最后都是mRebindRunnable.run()。
3. binding详解
3.1 binding机制
当在布局文件中绑定某个数据时,比如上面的 TextView 的 text 属性,在绑定时会自动接收兼容类型的参数所对应的方法,以 android:text="@{user.name}" 表达式为例,库会查找接受 user.getName() 所返回类型的setText(arg)方法。如果user.getName()的返回类型为String,则库会查找接受String参数的setText()方法。如果表达式返回的是int,则库会搜索接受int参数的 setText() 方法。表达式必须返回正确的类型,可以根据需要强制转换返回值的类型。
在布局文件中设置了属性,databinding 库会自动查找相关的setter方法进行设置,也就是说,如果以 TextView 为例,只有找到某个setter方法就可以进行验证了,TextView 中有一个 setError(error) 方法如下:
@android.view.RemotableViewMethod
public void setError(CharSequence error) {
if (error == null) {
setError(null, null);
} else {
Drawable dr = getContext().getDrawable(
com.android.internal.R.drawable.indicator_input_error);
dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
setError(error, dr);
}
}
这个方法主要用来提示错误信息,一般我们都是在代码中进行使用,这里我们把可以在demo中将该方法配置到布局文件中来使用,参考如下:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
app:error="@{user.name}"/>
3.2 BindingMethods
这是 databinding 库提供的一个注解,用于当 View 中的某个属性与其对应的setter方法名称不对应时进行映射,如TextView的属性 android:textColorHint 与之作用相同的方法是setHintTextColor方法,此时属性名称与对应的 setter 方法名称不一致,这就需要使用 BindingMethods 注解将该属性与对应的setter方法绑定,这样databinding就能够按照属性值找到对应的 setter 方法了,databinding已经处理了原生View中的像这种属性与 setter 方法不匹配的情况,来看一看源码中TextViewBindingAdapter对这些不匹配属性的处理,参考如下:
@BindingMethods({
@BindingMethod(type = TextView.class,
attribute = "android:autoLink", method = "setAutoLinkMask"),
@BindingMethod(type = TextView.class,
attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
@BindingMethod(type = TextView.class,
attribute = "android:editorExtras", method = "setInputExtras"),
@BindingMethod(type = TextView.class,
attribute = "android:inputType", method = "setRawInputType"),
@BindingMethod(type = TextView.class,
attribute = "android:scrollHorizontally", method = "setHorizontallyScrolling"),
@BindingMethod(type = TextView.class,
attribute = "android:textAllCaps", method = "setAllCaps"),
@BindingMethod(type = TextView.class,
attribute = "android:textColorHighlight", method = "setHighlightColor"),
@BindingMethod(type = TextView.class,
attribute = "android:textColorHint", method = "setHintTextColor"),
@BindingMethod(type = TextView.class,
attribute = "android:textColorLink", method = "setLinkTextColor"),
@BindingMethod(type = TextView.class,
attribute = "android:onEditorAction", method = "setOnEditorActionListener"),
})
所以,对于Android框架中View中的一些属性,databinding库已经使用BindingMethods已经做了属性自动查找匹配,那么当某些属性没有与之对应的setter方法时,如何在使用 databinding 时自定义 setter 方法呢,此时就要使用 BindingAdapter 了。
3.3 BindingAdapter
前面已经看到了executeBindings最后通过TextViewBindingAdapter的
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.btnDbFirstname, userFirstName)进行绑定。
BindingAdapter是基于APT注解技术做的,APT 能够在项目构建时根据相关编写规则生成特定代码完成一些指定功能的技术。BindingAdapter 有两个属性,value是一个String[],requireAll是一个boolean类型:value用来描述XML中感兴趣的关联属性,这里是个数组,说明一个扩展方法能够同时关注多个XML属性。requireAll 用来对 value 补充说明的,这个值默认是 true,表示使用这个适配规则必须在XML中声明该注解关注全部属性值,不然编译时会报错,而false则不须要,他容许只使用关注的部分或所有属性来使用该规则。
使用 BindingAdapter描述的函数签名,参数列表必须按照如下固定形式出现:
//类构建能够随意,APT会在构建时扫描全局代码
public class ViewAttrAdapter {
//注意XML标签关注了几个,参数列表就须要写几个对应的接受参数。且控件类必须在第一个参数。
@BindingAdapter({xml属性标签, ...})
public static void 函数名(关注的控件类 view, xml属性标签值 value, ...){
// 行为
}
}
在XML布局中,咱们应该使用BindingAdapter标签对应的标签类型进行赋值,且必须使用@{}做为值包裹,这是为了提供给APT解析XML是区别普通赋值的方式。
<View
android:layout_width="match_parent"
android:layout_height="175dp"
app:xml属性标签="@{函数参数接受类型的值}" />
需要注意的是绑定解析的触发规则:
// 这个限定使用时必定是android:src才能匹配
@BindingAdapter("android:src")
// 这个限定使用时 app:src 和 android:src 均可以匹配,注意的是,android必须是在自定义
// 属性声明的XML中有描述过的,如 attrs.xml 中
@BindingAdapter("src")
触发解析的属性必须标签="@{value}" 这里的@{}必定不能丢。
TextViewBindingAdapter的setText执行了判断内容是否变化的检查,只有内容变化了,才会真正调用TextView的setText方法,这里不再列出代码。如果我们有一个需求,想把TextView文字自动转成大写,我们可以自定义BindingAdapter:
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
view.setText(text.toString().toUpperCase());
}
注意,一定要用static方法,并且使用BindingAdapter注解,这样生成的绑定代码如下:
if ((dirtyFlags & 0x3L) != 0) {
// api target 1
com.xiaomi.zxm.databinding.DataBindingActivity1.setText(
this.btnDbFirstname, userFirstName);
this.btnDbFirstname.setError(userFirstName);
com.xiaomi.zxm.databinding.DataBindingActivity1.setText(
this.btnDbLastname, userLastName);
}
3.4 Binding自定义属性
以绑定imageUrl为例,先使用BindingAdater,单参数版本:
@BindingAdapter("imageUrl")
public static void setImageUrl(ImageView view, String url) {
Glide.with(view).load(url).into(view);
}
多参数版本:
@BindingAdapter(value = {"imageUrl", "placeholder"}, requireAll = false)
public static void setImageUrl(ImageView view, String url, Drawable placeHolder) {
Glide.with(view.getContext())
.load(url)
.placeholder(placeHolder)
.into(view);
}
然后在布局文件中使用imageUrl:
<?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>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
<!--自定义单个属性-->
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:imageUrl="@{user.photo}"
app:placeholder="@{@drawable/ic_launcher_background}" />
</LinearLayout>
</layout>
3.5 Binding事件
先定义一个监听事件的回调:
public class UserEventListener {
public void clickText(View view) {
Toast.makeText(view.getContext(), "点击了文本", Toast.LENGTH_LONG).show();
}
public void clickImage(View view) {
Toast.makeText(view.getContext(), "点击了图片", Toast.LENGTH_LONG).show();
}
}
xml中增加data variable信息和click绑定:
<data>
<variable name="user" type="com.xiaomi.zxm.databinding.User"/>
<variable name="userevent" type="com.xiaomi.zxm.databinding.UserEventListener"/>
</data>
<TextView
android:id="@+id/btn_db_lastname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background="#FFD56B"
android:onClick="@{userevent.clickText}"
android:text="@{user.lastName}" />
事件的绑定和属性的绑定很类似,接下来看生成的binding代码:
protected void executeBindings() {
...
if ((dirtyFlags & 0x6L) != 0) {
if (userevent != null) {
// read userevent::clickText
usereventClickTextAndroidViewViewOnClickListener =
(((mUsereventClickTextAndroidViewViewOnClickListener == null)
? mUsereventClickTextAndroidViewViewOnClickListener =
new OnClickListenerImpl()) :
mUsereventClickTextAndroidViewViewOnClickListener).setValue(userevent));
}
}
...
if ((dirtyFlags & 0x6L) != 0) {
// api target 1
this.btnDbLastname.setOnClickListener(
usereventClickTextAndroidViewViewOnClickListener);
}
...
}
// Listener Stub Implementations
public static class OnClickListenerImpl implements android.view.View.OnClickListener{
private com.xiaomi.zxm.databinding.UserEventListener value;
public OnClickListenerImpl setValue(
com.xiaomi.zxm.databinding.UserEventListener value) {
this.value = value;
return value == null ? null : this;
}
@Override
public void onClick(android.view.View arg0) {
this.value.clickText(arg0);
}
}
其根本还是在TextView上setOnClickListener,只不过其Listener包装了我们自己定义的UserEventListener,onClick的时候转成调用UserEventListener的clickText。
3.6 BindingConversion
在某些情况下,需要在特定类型之间进行自定义转换。例如,视图的 android:background 特性需要Drawable,但指定的color值是整数。我们看一个简单的例子,在每一个字符串后面加后缀:
@BindingConversion
public static String conversionString(String text) {
return text + "--BindingConversion";
}
和BindingAdapter类似,需要转换的方法也得是static的并且以注解BindingConversion声明,代码中保留之前通过BindingAdapter将文字都转成大写的部分,运行后发现TextView输入的zhang,转成了ZHANG-BINDINGCONVERSION,可见BindingAdapter和BindingConversion都生效了,且先执行BindingConversion,再执行BindingAdapter,再来看一个颜色的转换例子:
<TextView
android:id="@+id/btn_db_firstname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background='@{"红色"}'
android:text="@{user.firstName}"
app:error="@{user.firstName}"/>
在xml中中将background的背景色写成了红色,显然这个是编译不过的, 如果我们加上一下转换代码,就可以生效了:
@BindingConversion
public static Drawable convertStringToDrawable(String str) {
if (str.equals("红色")) {
return new ColorDrawable(Color.parseColor("#FF0000"));
}
return new ColorDrawable(Color.parseColor("#FFD56B"));
}
3.7 可观察数据
前面提到,ViewDataBinding继承了BaseObservable,实现了观察者模式,当调用setUser的时候,会调用如下代码进行刷新:
public void setUser(@Nullable com.xiaomi.zxm.databinding.User User) {
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
调用这里其实只是把整个User的东西进行了绑定,如果User对象的内容变化了,怎么刷新界面的显示呢?比如前面我们设置了Text上的click事件,会执行clickText, 在其实现中修改内容:
public class UserEventListener {
private User mUser;
public UserEventListener(User user) {
mUser = user;
}
public void clickText(View view) {
mUser.setLastName("xiaoai");
Toast.makeText(view.getContext(), "点击了文本", Toast.LENGTH_LONG).show();
}
}
运行后,可以看到界面并没有修改,这里我们需要对User改造一下,让它继承BaseObserver,并在需要监听的字段上增加Bindable:
public class ObservableUser extends BaseObservable {
@Bindable
private String mFirstName;
@Bindable
private String mLastName;
public ObservableUser(String firstName, String lastName) {
this.mFirstName = firstName;
this.mLastName = lastName;
}
public String getFirstName() {
return mFirstName;
}
public void setFirstName(String firstName) {
mFirstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public String getLastName() {
return mLastName;
}
public void setLastName(String lastName) {
mLastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
再看setUser的实现,updateRegistration会将这个生成的UserDataBindingImpl和ObservableUser(注意实现了Observable接口)关联起来:
public void setUser(@Nullable com.xiaomi.zxm.databinding.ObservableUser User) {
updateRegistration(0, User);
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
当调用ObservableUser的 notifyPropertyChanged(BR.lastName)时,会触发UserDataBindingImpl的如下方法:
@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
case 0 :
return onChangeUser((com.xiaomi.zxm.databinding.ObservableUser) object, fieldId);
}
return false;
}
private boolean onChangeUser(com.xiaomi.zxm.databinding.ObservableUser User,
int fieldId) {
if (fieldId == BR._all) {
synchronized(this) {
mDirtyFlags |= 0x1L;
}
return true;
} else if (fieldId == BR.firstName) {
synchronized(this) {
mDirtyFlags |= 0x4L;
}
return true;
} else if (fieldId == BR.lastName) {
synchronized(this) {
mDirtyFlags |= 0x8L;
}
return true;
}
return false;
}
private void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
if (mInLiveDataRegisterObserver) {
// We're in LiveData registration, which always results in a field change
// that we can ignore. The value will be read immediately after anyway, so
// there is no need to be dirty.
return;
}
boolean result = onFieldChange(mLocalFieldId, object, fieldId);
if (result) {
requestRebind();
}
}
最终由于Field变化,就会导致requestRebind重新刷新。updateRegistration这块逻辑比较复杂,不再详细列出来,其处理Field,List,Map,Live-Data的方法不太一样,前面的例子是处理Field的。
继承BaseObservable并在字段上增加Binderable注解并且set方法需要调用notifyPropertyChanged的方式比较繁琐,data-binding提供一种简化的方式:ObservableField,同样的,先修改User:
public class ObservableFieldUser {
private final ObservableField<String> mFirstName;
private final ObservableField<String> mLastName;
private final String mPhoto;
public ObservableFieldUser(ObservableField<String> firstName,
ObservableField<String> lastName, String photo) {
this.mFirstName = firstName;
this.mLastName = lastName;
this.mPhoto = photo;
}
public ObservableField<String> getFirstName() {
return mFirstName;
}
public ObservableField<String> getLastName() {
return mLastName;
}
public String getPhoto() {
return mPhoto;
}
}
字段改成ObservableField,注意不需要提供set方法,也不需要继承BaseObservable。Activity调用的地方:
ObservableFieldUser user = new ObservableFieldUser(
new ObservableField<String>("zhang"),
new ObservableField<String>("xinming"),
"https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png");
binding.setUser(user);
最后看click事件响应的地方:
public void clickText(View view) {
mUser.getLastName().set("xiaoai");
Toast.makeText(view.getContext(), "点击了文本", Toast.LENGTH_LONG).show();
}
mUer是ObservableFieldUser类型,直接getLastName获取ObservableField并调用set,界面就会刷新。生成的UserDataBindingImpl大同小异,只不过updateRegistration的位置不在setUser里,而在executeBinding中,这里不再列出来。
3.8 InverseBindingAdapter和双向绑定
绑定是将对象的数据显示到View上,通过上一节的方法,我们可以在对象更新后,刷新View,这个就是单向绑定,而双向绑定是View的数据变化后,其对象存储的数据也变化,要执行双向绑定,只要将xml的修改如下:
<EditText
android:id="@+id/btn_db_firstname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background='@{"红色"}'
android:text="@={user.firstName}"
app:error="@{user.firstName}"/>
绑定的时候加上=号,并且改成EditText,之后如果EditText修改就可以同时修改对象上的数据。还是看一下UserDataBindingImpl的生成代码:
protected void executeBindings() {
...
if ((dirtyFlags & 0x10L) != 0) {
// api target 1
androidx.databinding.adapters.ViewBindingAdapter.setBackground(
this.btnDbFirstname,
com.xiaomi.zxm.databinding.DataBindingActivity1.convertStringToDrawable("红色"));
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher
(this.btnDbFirstname,
(androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null,
(androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null,
(androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null,
btnDbFirstnameandroidTextAttrChanged);
}
private androidx.databinding.InverseBindingListener
btnDbFirstnameandroidTextAttrChanged = new
androidx.databinding.InverseBindingListener() {
@Override
public void onChange() {
java.lang.String callbackArg_0 =
androidx.databinding.adapters.TextViewBindingAdapter.getTextString(
btnDbFirstname);
boolean userJavaLangObjectNull = false;
androidx.databinding.ObservableField<java.lang.String> userFirstName = null;
com.xiaomi.zxm.databinding.ObservableFieldUser user = mUser;
java.lang.String userFirstNameGet = null;
boolean userFirstNameJavaLangObjectNull = false;
userJavaLangObjectNull = (user) != (null);
if (userJavaLangObjectNull) {
userFirstName = user.getFirstName();
userFirstNameJavaLangObjectNull = (userFirstName) != (null);
if (userFirstNameJavaLangObjectNull) {
userFirstName.set(((java.lang.String) (callbackArg_0)));
}
}
}
};
}
可以看到,通过TextViewBindingAdapter给EditText添加了TextWatcher,注意最后一个参数是一个回调,我们看看TextWatcher什么时候会回调:
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
final OnTextChanged on, final AfterTextChanged after,
final InverseBindingListener textAttrChanged) {
newValue = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int 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();
}
}
...
}
当执行onTextChanged的时候,回调onChange,注意,前面的例子中加过BindingAdapter和BindingConversion:
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
view.setText(text.toString().toUpperCase());
}
@BindingConversion
public static String conversionString(String text) {
return text + "--BindingConversion";
}
一定要去掉,否则会死循环,以BindingAdapter为例,开始文本是zhang,经过BindingAdapter,变成ZHANG,触发mUser的字段变更,这个变更又会触发BindingAdapter,然后又修改mUser,如此循环,当然,根本原因是我们实现的BindingAdapter没有进行oldText和text的字符串,判断是否相等,而TextViewBindingAdapter中的TextWatcher的onTextChanged接口也没有判断文本是否相同。
InverseBindingAdapter和BindingAdapter类似,只是用于反向绑定的,这里不再给例子。
3.9 绑定Live-Data
前面两节讲了怎么实现单向绑定和双向绑定,细心的同学是不是发现ObservableField和之前介绍的LiveData很类似。实际上,可以使用 LiveData 对象作为数据绑定来源,自动将数据变化通知给界面。
与实现Observable的对象(例如可观察字段)不同,LiveData 对象了解订阅数据更改的观察器的生命周期。了解这一点有许多好处,具体说明请参阅使用LiveData的优势。在 Android Studio 版本 3.1 及更高版本中,可以在数据绑定代码中将可观察字段替换为LiveData对象。
要将 LiveData 对象与绑定类一起使用,需要指定生命周期所有者来定义 LiveData对象的范围。下面是一个例子,先看LiveData对象:
public class LiveDataUserModel extends ViewModel {
private static final String TAG = "LiveDataUserModel";
private MutableLiveData<String> mFirstName;
private MutableLiveData<String> mLastName;
public LiveDataUserModel() {
this("zhang", "xinming");
}
public LiveDataUserModel(String firstName, String lastName) {
mFirstName = new MutableLiveData<>();
mFirstName.setValue(firstName);
mLastName = new MutableLiveData<>();
mLastName.setValue(lastName);
}
public MutableLiveData<String> getFirstName() {
return mFirstName;
}
public void setFirstName(String firstName) {
mFirstName.postValue(firstName);
}
public MutableLiveData<String> getLastName() {
return mLastName;
}
public void setLastName(String lastName) {
mLastName.postValue(lastName);
}
@Override
protected void onCleared() {
super.onCleared();
Log.d(TAG, "onCleared()");
}
}
需要继承ViewModel,并将需要观察的数据包装成MutableLiveData,接下来看Activity中怎么使用:
public class LiveDataBindingActivity extends AppCompatActivity {
private static final String TAG = "LiveDataBindingActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LiveUserDataBinding binding = DataBindingUtil.setContentView(
this, R.layout.live_user_data);
binding.setLifecycleOwner(this);
ViewModelProvider vmp = new ViewModelProvider(this,
ViewModelProvider.AndroidViewModelFactory.getInstance(
this.getApplication()));
LiveDataUserModel liveDataUserModel = vmp.get(LiveDataUserModel.class);
binding.setUser(liveDataUserModel);
final MutableLiveData<String> liveFirstName = liveDataUserModel.getFirstName();
liveFirstName.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String firstName) {
Log.d(TAG, "firstName=" + firstName);
}
});
}
}
setLifecycleOwner,如果不设置,实际上也能运行,但是就没有了生命周期的管理。
xml的部分和之前的完全一致,只需要修改data下variable的type是最新的LiveDataUserModel 就行。
至于后台的实现,其实还是看LiveUserDataBindingImpl类,其实也是在TextWatcher中注入了InverseBindingListener的回调,这里不再列出代码,唯一的差别是,在executeBindings方法中,通过updateLiveDataRegistration(不是updateRegistration)将databinding和LiveData的字段关联起来。
3.10 表达式
参考:https://developer.android.com/topic/libraries/data-binding/expressions
三.总结
databinding是实现MVVM模式的一种数据绑定框架,自然拥有MVVM的优点,那么databinding有哪些缺点呢?
1、databinding的View层UI显示是在xml文件中实现的,当出现问题时,无法确定是Model的问题还是xml的问题,而且xml中也不方便调试。
2、基于数据驱动的databinding ,在比较大的模块时,Model层数据可能会比较大。而且在释放的时候不能像其他模式一样,不同View的数据无法根据各自View的生命周期来进行释放,而是在整个页面销毁时统一释放,这样就会占用更大内存。
3、对于开发者而言,可能更易接受这种MVVM模式的绑定方式,而不是在布局文件中直接进行数据和事件的绑定操作。
在Databinding中,ViewModel会持有ObservableField等的数据,View监听ObservableField的数据变化,而当ViewModel中ObservableField数据变化时即会通知UI进行相应更新。在不使用databinding时,就可以通过LiveData起到相同的作用。