Data Binding - xiaoniudonghe2015/Android-Java-Code-Style GitHub Wiki
Data Binding,顾名思义,数据绑定,是Google对MVVM在Android上的一种实现,可以直接绑定数据到xml中,并实现自动刷新。还支持双向绑定,尽管使用场景不是那么多。 Data Binding可以提升开发效率(节省很多以往需要手写的java代码),性能高(甚至超越手写代码),功能强(强大的表达式支持)。
去掉Activities & Fragments内的大部分UI代码(setOnClickListener, setText, findViewById, etc.)
XML变成UI的唯一真实来源
减少定义view id的主要用途(数据绑定直接发生在xml)
UI代码放到了xml中,布局和数据更紧密
性能超过手写代码
保证执行在主线程
IDE支持还不那么完善(提示、表达式)
报错信息不那么直接
重构支持不好(xml中进行重构,java代码不会自动修改)
使用起来实在很简单,在app模块的build.gradle中加上几行代码就行了。
android {
…
dataBinding {
enabled = true
}
}把一个普通的layout变成data binding layout也只要几行的修改:
<layout>
// 原来的layout
</layout>在xml的最外层套上layout标签即可,修改后就可以看到生成了该布局对应的*Binding类。
Binding生成规则 默认生成规则:xml通过文件名生成,使用下划线分割大小写。 比如activity_demo.xml,则会生成ActivityDemoBinding,item_search_hotel则会生成ItemSearchHotelBinding。 view的生成规则类似,只是由于是类变量,首字母不是大写,比如有一个TextView的id是first_name,则会生成名为firstName的TextView。 我们也可以自定义生成的class名字,只需要:
<data class=“ContactItem”>
…
</data>这样生成的类就会变成ContactItem。
所有Binding实例的生成都可以通过DataBindingUtil进行,方法名与该view的原inflate方法一致,如activity仍然为setContentView,只是增加了参数因为需要获得activity。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityDemoBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_demo);
}使用了Data Binding后,我们再也不需要findViewById,因为一切有设置id的view,都已经在Binding类中被初始化完成了,只需要直接通过binding实例访问即可。没有设置id的view不会出现Binding类中,根布局除外.
使用data标签,我们就可以在xml中申明变量,在其中使用该变量的field,并通过binding实例set进来。 如:
<data>
<variable
name="employee"
type="com.github.markzhai.databindingsample.Employee"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".DemoActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{employee.lastName}"
android:layout_marginLeft="5dp"/>
</LinearLayout>public class Employee {
private String mLastName;
private String mFirstName;
public Employee(String lastName, String firstName) {
mLastName = lastName;
mFirstName = firstName;
}
public Employee(String lastName, String firstName, boolean fired) {
mLastName = lastName;
mFirstName = firstName;
}
@Bindable
public String getLastName() {
return mLastName;
}
public void setLastName(String lastName) {
mLastName = lastName;
}
@Bindable
public String getFirstName() {
return mFirstName;
}
public void setFirstName(String firstName) {
mFirstName = firstName;
}
}然后我们就可以在java代码中使用
binding.setEmployee(employee);
// 或者直接通过setVariable
binding.setVariable(BR.employee, employee);严格意义上来说,事件绑定也是一种变量绑定。我们可以在xml中直接绑定
android:onClick
android:onLongClick
android:onTextChanged …
不可以通过对象.变量.方法调用 通常会在java代码中定义一个名为Handler或者Presenter的类,然后set进来,方法签名需和对应listener方法一致。
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="employee"
type="com.github.markzhai.databindingsample.Employee"/>
<variable
name="presenter"
type="com.github.markzhai.databindingsample.DemoActivity.Presenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".DemoActivity">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入 First Name"
android:onTextChanged="@{presenter::onTextChanged}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{presenter.onClick}"
android:text="@{employee.firstName}"/>
</LinearLayout>
</layout>在Java代码中:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
binding.setPresenter(new Presenter());
...
}
public class Presenter {
public void onTextChanged(CharSequence s, int start, int before, int count) {
employee.setFirstName(s.toString());
employee.setFired(!employee.isFired.get());
}
public void onClick(View view) {
Toast.makeText(DemoActivity.this, "点到了", Toast.LENGTH_SHORT).show();
}
}可以不遵循默认的方法签名:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:visibility="@{employee.isFired ? View.GONE : View.VISIBLE}"
android:onClick="@{() -> presenter.onClickListenerBinding(employee)}"/>public class Presenter {
public void onClickListenerBinding(Employee employee) {
Toast.makeText(DemoActivity.this, employee.getLastName(),
Toast.LENGTH_SHORT).show();
}
}0反射
findViewById需要遍历整个viewgroup,而现在只需要做一次就可以初始化所有需要的view
使用位标记来检验更新(dirtyFlags)
数据改变在下一次批量更新才会触发操作
表达式缓存,同一次刷新中不会重复计算
算术 + - / * %
字符串合并 +
逻辑 && ||
二元 & | ^
一元 + - ! ~
移位 >> >>> <<
比较 == > < >= <=
Instanceof
Grouping ()
文字 - character, String, numeric, null
Cast
方法调用
Field 访问
Array 访问 []
三元 ?:
尚且不支持this, super, new, 以及显示的泛型调用。 值得一提的是还有空合并运算符,如
android:text=“@{user.displayName ?? user.lastName}”会取第一个非空值作为结果。
这里举一个常见的例子,某个view的margin是其左侧ImageView的margin加上该ImageView的宽度,以往我们可能需要再定义一个dimension来放这两个值的合,现在只需要
android:marginLeft="@{@dimen/margin + @dimen/avatar_size}"就搞定了。
我们甚至还可以直接组合字符串,如:
android:text="@{@string/nameFormat(firstName, lastName)}"
<string name="nameFormat">%s, %s</string>data binding会自动帮助我们进行空指针的避免,比如说@{employee.firstName},如果employee是null的话,employee.firstName则会被赋默认值(null)。int的话,则是0。
需要注意的是数组的越界,毕竟这儿是xml而不是java,没地方让你去判断size的。
<include layout=“@layout/name” bind:user="@{user}"/>对于include的布局,使用方法类似,不过需要在里面绑定两次,外面include该布局的layout使用bind:user给set进去。 这里需要注意的一点是,被include的布局必须顶层是一个ViewGroup,目前Data Binding的实现,如果该布局顶层是一个View,而不是ViewGroup的话,binding的下标会冲突(被覆盖),从而产生一些预料外的结果。
一个纯净的Java ViewModel类被更新后,并不会让UI去更新。而数据绑定后,我们当然会希望数据变更后UI会即时刷新,Observable就是为此而生的概念。 类继承BaseObservable:
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}BaseObservable提供了一系列notify函数(其实就是notifyChange和notifyPropertyChanged),前者会刷新所有的值域,后者则只更新对应BR的flag,该BR的生成通过注解@Bindable生成,在上面的实例代码中,我们可以看到两个get方法被注释上了,所以我们可以通过BR访问到它们并进行特定属性改变的notify。
如果所有要绑定的都需要创建Observable类,那也太麻烦了。所以Data Binding还提供了一系列Observable,包括 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和ObservableParcelable。我们还能通过ObservableField泛型来申明其他类型,如:
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}而在xml中,使用方法和普通的String,int一样,只是会自动刷新,但在java中访问则会相对麻烦:
user.firstName.set("Google");
int age = user.age.get();相对来说,每次要get/set还是挺麻烦,私以为还不如直接去继承BaseObservable。
有一些应用使用更动态的结构来保存数据,这时候我们会希望使用Map来存储数据结构。Observable提供了ObservableArrayMap:
public class Employee extends BaseObservable {
public ObservableArrayMap<String, String> user = new ObservableArrayMap<>();
public Employee() {
user.put("hello", "world");
user.put("hi", "world");
user.put("yo", "world");
}
}而在xml中,我们可以直接通过下标key访问它们:
<data>
<variable
name="employee"
type="com.github.markzhai.sample.Employee"/>
</data>
…
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text='@{employee.user["hello"]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text='@{employee.user["hi"]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text='@{employee.user["yo"]}'/>当我们不想定义key的时候,可以使用ObservableArrayList:
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);layout中直接通过数字下标进行访问。
App中经常用到列表展示,Data Binding在列表中一样可以扮演重要的作用,直接绑定数据和事件到每一个列表的item。
过去我们往往会使用ListView、GridView、或者GitHub上一些自定义的View来做瀑布流。自从RecyclerView出现后,我们有了新选择,只需要使用LayoutManager就可以。RecyclerView内置的垃圾回收,ViewHolder、ItemDecoration装饰器机制都让我们可以毫不犹豫地替换掉原来的ListView和GridView。
我们只需要定义一个基类ViewHolder,就可以方便地使用上Data Binding:
public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
protected final T mBinding;
public BindingViewHolder(T binding) {
super(binding.getRoot());
mBinding = binding;
}
public T getBinding() {
return mBinding;
}
} @Override
public BindingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(mLayoutInflater,
R.layout.item_employee, parent, false);
return new BindingViewHolder(binding);
}public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}executePendingBindings会强制立即刷新绑定的改变。 Adapter可以直接使用该ViewHolder,或者再继承该ViewHolder,T使用具体Item的Binding类(以便直接访问内部的View)。至于Listener,可以在onBindViewHolder中进行绑定,做法类似于普通View。
就像Data Binding会自动去查找get方法一下,在遇到属性绑定的时候,它也会去自动寻找对应的set方法。 拿DrawerLayout举一个例子:
<android.support.v4.widget.DrawerLayout
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
app:scrimColor=“@{@color/scrimColor}”/>如此,通过使用app命名空间,data binding就会去根据属性名字找对应的set方法,scrimColor -> setScrimColor:
public void setScrimColor(@ColorInt int color) {
mScrimColor = color;
invalidate();
}如果找不到的话,就会在编译期报错。 利用这种特性,对一些第三方的自定义View,我们就可以继承它,来加上我们的set函数,以对其使用data binding。 比如Fresco的SimpleDraweeView,我们想要直接在xml指定url,就可以加上:
public void setUrl(String url) {
view.setImageURI(TextUtils.isEmpty(url) ? null : Uri.parse(url));
}这般,就能直接在xml中去绑定图片的url。这样是不是会比较麻烦呢,而且有一些系统的View,难道还要继承它们然后用自己实现的类?其实不然,我们还有其他方法可以做到自定义属性绑定。
如果没有对应的set方法,或者方法签名不同怎么办?BindingAdapter注释可以帮我们来做这个。 事实上这个Adapter已经由Data Binding实现好了,可以在android.databinding.adapters.ViewBindingAdapter看到有很多定义好的适配器,还有BindingMethod。如果需要自己再写点什么,仿照这些来写就好了。 我们还可以进行多属性绑定,比如
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
} <ImageView
android:id="@+id/avatar"
android:layout_width="150dp"
android:layout_height="150dp"
android:visibility="@{employee.isFired ? View.INVISIBLE : View.VISIBLE}"
app:imageUrl="@{employee.avatar}"
app:placeholder="@{@drawable/default_avatar}"/>来使用Picasso读取图片到ImageView。
过去,我们需要自己定义Listener来做双向绑定:
<EditText android:text=“@{user.name}”
android:afterTextChanged=“@{callback.change}”/>public void change(Editable s) {
final String text = s.toString();
if (!text.equals(name.get()) {
name.set(text);
}
}需要自己绑定afterTextChanged方法,然后检测text是否有改变,有改变则去修改observable。
现在可以直接使用@=(而不是@)来进行双向绑定了,使用起来十分简单
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
>
<data>
<variable
name="model"
type="com.github.markzhai.sample.FormModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:text="@={model.name}"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:text="@={model.password}"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{@string/welcome(model.name)}"/>
</LinearLayout>
</layout>public class FormModel extends BaseObservable {
private String mName;
private String mPassword;
public FormModel(String name, String password) {
mName = name;
mPassword = password;
}
@Bindable
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
notifyPropertyChanged(com.github.markzhai.sample.BR.name);
}
@Bindable
public String getPassword() {
return mPassword;
}
public void setPassword(String password) {
mPassword = password;
notifyPropertyChanged(com.github.markzhai.sample.BR.password);
}
}这样,我们对这个EditText的输入,就会自动set到对应model的name字段上。
<ImageView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>可以简化为:
<ImageView android:id=“@+id/avatar”
android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{avatar.visibility}”/>
<CheckBox android:visibility="@{avatar.visibility}"/><CheckBox android:id=”@+id/seeAds“/>
<ImageView android:visibility=“@{seeAds.checked ?
View.VISIBLE : View.GONE}”/>这样CheckBox的状态变更后ImageView会自动改变visibility。
除了直接使用方法引用,在Presenter中写和OnClickListener一样参数的方法,我们还能使用Lambda表达式:
android:onClick=“@{(view)->presenter.save(view, item)}”
android:onClick=“@{()->presenter.save(item)}”
android:onFocusChange=“@{(v, fcs)->presenter.refresh(item)}”我们还可以在lambda表达式引用view id(像上面表达式链那样),以及context。
使用data binding后,我们还能自动去做transition动画:
binding.addOnRebindCallback(new OnRebindCallback() {
@Override
public boolean onPreBind(ViewDataBinding binding) {
ViewGroup sceneRoot = (ViewGroup) binding.getRoot();
TransitionManager.beginDelayedTransition(sceneRoot);
return true;
}
});这样,当我们的view发生改变,比如visibility变化的时候,就能看到一些transition动画。
对新项目,不要犹豫,直接上。
对于老的项目,可以替换ButterKnife这种库,从findViewById开始改造,逐渐替换老代码。
callback绑定只做事件传递,NO业务逻辑,比如转账
保持表达式简单(不要做过于复杂的字符串、函数调用操作)
对于老项目,可以进行以下的逐步替换:
逐步替换findViewById,取而代之地,使用binding.name, binding.age直接访问View。
引入variable,把手动在代码对View进行set替换为xml直接引用variable。
使用Presenter/Handler类来做事件的绑定。
创建ViewModel类来进行即时的属性更新触发UI刷新。
运用双向绑定来简化表单的逻辑,将form data变成ObservableField。这样我们还可以在xml做一些酷炫的事情,比如button仅在所有field非空才为enabled(而过去要做到这个得加上好几个EditText的OnTextChange监听)。