private ActivityMainBinding mBinding;
protected void onCreate(Bundle savedInstanceState) {
mBinding = DataBindingUtil.setContentView(this,
R.layout.activity_main);
}
public void updateUI(User user) {
mBinding.setUser(user);
}
<layout>
<data>
<variable name="user"
type="com.android.example.User"/>
</data>
<LinearLayout …>
<TextView android:text="@{user.name}"/>
<TextView android:text="@{user.lastName}"/>
<TextView android:text='@{"" + user.age}'/>
</LinearLayout>
</layout>
为什么要使用 Data Binding
Android 开发的 Data Binding 库可以使开发者以最小的力气,快速构建丰富的具有响应性的用户体验。通过删除 样板数据驱动的用户界面,说明如何使用 Data Binding 改善您应用程序的开发,使代码更加干净优雅。可以切确地说:只要你在 Android 开发中使用 Data Binding,很快你就会感觉到它所带来到好处。
但是它怎么工作的呢?
Data Binding 做的第一件事就是进入并处理您的布局文件,其中的“进入”指的是,在你的程序代码正在被编译的过程中,它会找出布局文件中所有关于它的内容,获取到它所需要的信息,然后删掉它们,删掉它们的原因是如果继续存着视图系统并不认得它们。
第二步骤就是通过语法来解析这些表达式,例如:
<TextView android:visibility="@user.isAdmin ? View.VISIBLE : View.GONE}"/>
它的工作就是从这些文件中把东西解析出来,了解里面有什么。
第三步就是在你代码编译过程中解决相关依赖问题。在这一步中,例如,我们看一下 user.isAdmin
,想:“这是在运行时获取到 User 类对象中的一个布尔值。”
最后一步就是 Data Binding 会自动生成一些你不需要再写的类文件
Data Binding 特别吸引人的地方
Data Binding 使你的工作更加简单。让我们来看一下我们所支持的语言:支持绝大部分的 Java 写法,它允许变量数据访问、方法调用、参数传递、比较、通过索引访问数组,甚至还支持三目运算表达式。 这基本上能够满足你的需求,但也有一些事情无法办到,比如 new
,我们真的不想你在布局中的表达式里写 new
.
也会自动检查是否为 null,如果你想访问 contact 的 name 变量,而 contact 是 null 的,以前你不得不痛苦地写contact null ? null : contact.friend null ? :
,而现在,如果 contact 是 null 的,那么整个表达式就是 null 的,你无需判断也不会出错。
我们还提供了一个 null 的合并运算符号 ??
,这是一个三目运算符的简便写法:
contact.lastName ?? contact.name
contact.lastName != null ? contact.lastName : contact.name
它表达的是,如果左边不是 null 的,那么使用左边的值,否者使用右边的值。
我们还可以使用中括号操作符来访问 list 或者 map,如果你有使用 contacts[0]
,它有可能是一个 list 或者是一个数组,都可以。使用中括号来访问项目,更加容易简洁。
资源内容
我们希望你能在你的表达式中使用资源引用内容,因此你可以在你的表达式中使用资源和字符串格式化方法。
在表达式中:
android:padding="@{isBig ? @dimen/bigPadding : @dimen/smallPadding}"
内联字符串格式:
android:text="@{@string/nameFormat(firstName, lastName)}"
内联复数:
android:text="@{@plurals/banana(bananaCount)}"
事件处理
我们使用 Data Binding 也是支持点击响应的。你可以使用 clicked
,同时,另外的一些事件也都支持。支持到 Android 2.3 版本。你还可以做一些偏门的监听器,比如 onTextChanged
,TextWatcher 有三个方法,但大部分人都只关心它的 onTextChanged
,借助 Data Binding,你可以直接访问任意一个或全部你想访问的:
<Button android:onClick="clicked" …/>
<Button android:onClick="@{handlers.clicked}" …/>
<Button android:onClick="@{isAdult ? handlers.adultClick : handlers.childClick}" …/>
<Button android:onTextChanged="@{handlers.textChanged}" …/>
可观测性
首先我们需要创建一个项目,包含一些可以被观测的对象。在这里,我已经继承了个 BaseObservable,然后我们在这个新的类当中加入我们的属性。
public class Item extends BaseObservable {
private String price;
@Bindable
public String getPrice() {
return this.name;
}
public void setPrice(String price) {
this.price = price;
notifyPropertyChanged(BR.price);
}
}
我们使用 notifyPropertyChanged
来进行数据改变完成通知,但我们怎么通知一个数据即将改变?我们不得不写一个 @Bindable
注解在 getPrice
。这将会自动产生一个 BR.price
,这个 BR 很像我们经常使用的 R 类文件,我们通过这些注解会自动生成它。但是,你可能不想让我们入侵你的整个代码体系,所以我们允许你去实现这些可被观测的类。自己实现的示例如下:
public class Item implements Observable {
private PropertyChangeRegistry callbacks = new …
…
@Override
public void addOnPropertyChangedCallback(
OnPropertyChangedCallback callback) {
callbacks.add(callback);
}
@Override
public void removeOnPropertyChangedCallback(
OnPropertyChangedCallback callback) {
callbacks.remove(callback);
}
}
我们有一个很方便的类 PropertyChangedRegistry
,让你很方便地进行回调和通知。你们中的一些人可能会觉得这是一件很讨厌的事情,他们更多的是想要一个可观测的属性变量。从本质上讲,它们每一个都是一个可观测都对象。你可以很方便地说,accessImage, 它实际上就会去访问对应图片地内容,如果你要访问 price,它就会去访问价格的字符串内容。
关于这些对象的特别之处在于,以前使用 Java 代码,你必须得调用 set 或者 get 方法,但在你的绑定表达式中,你只要写 item.price
,我们能够自动知道你需要调用它的 getter 方法,所以当价格发生变化时,它才设定它的值。
public class Item {
public final ObservableField<Drawable> image =
new ObservableField<>();
public final ObservableField<String> price =
new ObservableField<>();
public final ObservableInt inventory =
new ObservableInt();
}
item.price.set("$33.41");
在其他情况下,你可能有更多的“细碎、不确定”的数据,这通常发生在你开发周期的一开始,特别是在原型阶段,可能你会有很多键值数据对,并且你很不想定义这些类,所以你可能会想使用 map 来存储这些数据对。这种情况下,你可以使用一个可观测的 ObservableMap 来放你的项目,然后就可以访问它们了。不幸的是,你只能使用中括号类似访问数组那样访问它们:
ObservableMap<String, Object> item =
new ObservableArrayMap<>();
item.put("price", "$33.41");
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{item["price"]}'/>
性能
对于性能的最重要的方面是,我们基本上是零反射。一切都发生在编译期。
其次,你还可以得到一些额外的好处。让我们来讨论在一个布局中使用数据绑定,你把一个对象命名为price,然后价格变化了。新的价格来了,通知来了。数据绑定会更新 TextView,TextView 自动进行重新测量。如果你是用手工写这些功能的代码,那你就不太可能写下这样的代码,而是会重新关闭再加载新内容。所以这是一个额外的好处。
关于性能的另一个作用是针对 findById
。当你在Android系统上使用代码findById
,你的代码findViewById
对于其他 View 一次一次不断循环查找。
当你初始化数据绑定时,我们实际上知道在编译时我们对应的 View,所以我们有一个方法来找到我们想要的所有 View。我们只需遍历布局层次一次,就可以收集到所有的 View。这个计算只对于一个布局只发生一次。当第二次我们需要另一个其中的子 View 的时候,不需要再次遍历所有子 View,因为我们已经找到了所有的 View。
性能有时关键就在一些小细节上。你在你的代码中包含一个库,一些行为会改变,但有时也会有一些成本。但有了这些性能上的优化,我认为我们做了它值得了,甚至有时比你写的代码还好。
RecyclerView 和 Data Binding 多种类型视图
<layout>
<data>
<variable name="data" type="com.example.Photo"/>
</data>
<ImageView android:src="@{data.url}" …/>
</layout>
如果你需要另一种类型的结果,比如说叫“place”,那么你就需要有一个完全不同的布局,另一个XML文件:
<layout>
<data>
<variable name="data" type="com.example.Place"/>
</data>
<ImageView android:src="@{data.url}" …/>
</layout>
这两个布局文件之间唯一共享的,就是变量名,即所谓的“data”。
不幸的是,在这个基类当中并没有一个 setPlace
方法,但幸运的是我们提供了另外一个 API 叫做 setVariable
:
public void bindTo(Object obj) {
mBinding.setVariable(BR.data, obj);
mBinding.executePendingBindings();
}
你可以提供需要绑定的目标 ID 和对应的整个对象,自动生成的代码会去检查它的类型,并将其赋值。
这个 setVariable
看起来像这样:“如果传进来的 ID 是我知道的,我就将它转型并且赋值。”
boolean setVariable(int id, Object obj) {
if (id == BR.data) {
setPhoto((Photo) obj);
return true;
}
return false;
}
对于 onBind
, onCreate
方法类似以往的写法,比较特别的就是 getItemViewType
,我们返回布局文件的 ID 作为这个方法的返回值。RecyclerView 会将它传递给 onCreateViewHolder
,onCreateViewHolder
将通过 DataBindingUtil 去创建对应的绑定类。这样所有项目都会有它们的布局,你不需要手动去创建这些不同布局类型的对象了。
DataBoundViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
return DataBoundViewHolder.create(mLayoutInflater, viewGroup, type);
}
void onBindViewHolder(DataBoundViewHolder viewHolder, int position) {
viewHolder.bindTo(mDataList.get(position));
}
public int getItemViewType(int position) {
Object item = mItems.get(position);
if (item instanceof Place) {
return R.layout.place_layout;
} else if (item instanceof Photo) {
return R.layout.photo_layout;
}
throw new RuntimeException("invalid obj");
}
当然,如果你是在一个生产应用程序中,你可能会保留做实例检查。你应该有一个基本的类,知道如何返回布局,这是通常的想法。
public class AllBindingAdapter extends RecyclerView.Adapter<AllBindingAdapter.DataBoundViewHolder>{ private List<Object> allNames; public AllBindingAdapter(List<Object> list) { this.allNames = list; } @NonNull @Override public DataBoundViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return DataBoundViewHolder.create(LayoutInflater.from(parent.getContext()), parent, viewType); } @Override public void onBindViewHolder(@NonNull DataBoundViewHolder holder, int position) { holder.bindTo(allNames.get(position)); } @Override public int getItemCount() { return allNames.size(); } @Override public int getItemViewType(int position) { Object item = allNames.get(position); if (item instanceof BlankBean) { return R.layout.item_all_layout; } else if (item instanceof Place) { return R.layout.item_all_layout1; } throw new RuntimeException("invalid obj"); } public static class DataBoundViewHolder extends RecyclerView.ViewHolder { private ViewDataBinding mBinding; public DataBoundViewHolder(ViewDataBinding binding) { super(binding.getRoot()); mBinding = binding; } public ViewDataBinding getBinding() { return mBinding; } public void bindTo(Object obj) { mBinding.setVariable(BR.data,obj); mBinding.executePendingBindings(); } public static DataBoundViewHolder create(LayoutInflater from, ViewGroup parent, int viewType){ ViewDataBinding viewDataBinding = null; if (viewType == R.layout.item_all_layout){ viewDataBinding = ItemAllLayoutBinding.inflate(from,parent,false); }else if (viewType == R.layout.item_all_layout1){ viewDataBinding = ItemAllLayout1Binding.inflate(from,parent,false); } return new DataBoundViewHolder(viewDataBinding); } } }
绑定适配器和回调 (31:27)
根据民意测验,接下来讲的可能是最酷最受欢迎的特点…(我所做的)。它甚至可能是 Android 开发中最酷的功能。好吧,也许是我在炒作,哈哈。
让我们想象一下,有什么比 setText
更复杂,或许可以是一个图像的URL。你想设置为 ImageView,你需要设置一个图片的网址。当然,你不想在 UI 线程上做这件事(请记住,这些东西是在 UI 线程上进行评估)。你想用 Picasso 或其他图片加载库。于是或许你会这么做:
<ImageView …
android:src="@{contact.largeImageUrl}" />
这基本上无法正常工作。context 要从哪里来,你把它放在什么地方?这里取不到 view。所以正确的写法应该是创建一个 BindingAdapter:
@BindingAdapter("android:src")
public static void setImageUrl(ImageView view, String url) {
Picasso.with(view.getContext()).load(url).into(view);
}
现在这里的 BindingAdapter 是一个注解。这是为了我们设置的 android:src 和 Android 源码的关联。这是一个静态的方法,它需要2个参数。它需要一个View 和一个字符串,但注意它也可以采取其他类型。如果你想要一个不同的方法,接受一个 int 参数或 drawable,你也是可以做到的。你可以把任何你想要的填入这里。在这种情况下,我们已经支持了 Picasso 这个库的使用了。我们所有的代码都在那里。既然我们有了这个 View,我们就可以得到它的背景。我们可以写任何我们想写的代码。现在我们可以在用户界面上把图像加载出来,如你所愿。
利用属性 (33:12)
你可能想做一些更复杂的内容,比如说,我们想设置一个 PlaceHolder,设置它的图片源文件,或者它的图片 URL:
<ImageView …
android:src="@{contact.largeImageUrl}"
app:placeHolder="@{R.drawable.contact_placeholder}"/>
我们有两个不同的属性,同时他们对应的有两个不同的静态方法,但数据绑定却不知道,所以目前还不能直接如你所愿地进行工作。实际上,现在我们在 BindingAdapter 注解中有两个同样的属性,你只要取得这里他们的值,把他们传递给 Picasso 正确的方法即可:
<ImageView …
android:src="@{contact.largeImageUrl}"
app:placeHolder="@{R.drawable.contact_placeholder}"/>
@BindingAdapter(value = {"android:src", "placeHolder"},
requireAll = false)
public static void setImageUrl(ImageView view, String url,
int placeHolder) {
RequestCreator requestCreator =
Picasso.with(view.getContext()).load(url);
if (placeHolder != 0) {
requestCreator.placeholder(placeHolder);
}
requestCreator.into(view);
}
如果你现在有三个属性呢?有一个是 Android 源码的图片源属性,一个是占位图片属性(PlaceHolder),还有一个是设置图像的 URL 属性。所有这些不同的 BindingAdapter。真的,我有点太懒了,所以让我们做点别的了。我们可以有一个 BindingAdapter 注解需要所有这些属性,也可以是只要一个或两个,或它们的任意组合。我们所要做的就是把所需的一切都设为 false,然后我们获得所有这些参数。如果有值就会传递过来,如果没有提供,它会将它们作为默认值传递给它们。如果你没有在你的布局中写一个占位符属性,那么占位符将为零。我们在 Picasso 中调用 setter 的时候会进行检查。