Android DataBinding 详解
前几天小试牛刀写了一篇 Android DataBinding 初探,只是简单的介绍了一下 DataBinding 的几个小问题,并没有特别详细的去介绍 DataBinding 的更多方法,这几天看了一下 DataBinding 的官网的相关内容,觉得有必要把官网的用法记录一下,用来参考及以后使用时的参考,以前大家很多人都使用过注解框架,包括 Jake Wharton 大神的 ButterKnife,但 DataBinding 出来后,相信会对此类的框架形成碾压,毕竟是 Google 的官方出品,接下来我们切入正题了,开始详细的去介绍这个 Android DataBinding Library,先上一张概况图:
一、构建环境(Build Environment)
- 要使用 DataBinding 数据库,先从 Android SDK Manager 的支持库里下载该库
- 配置你的应用程序使用数据绑定,在应用程序模块,你的 build.gradle 文件添加数据绑定元素
- 另外,需要注意你使用的 Android Studio 的兼容版本,需要 1.3 及以上的版本
- android {
- ....
- dataBinding {
- enabled = true
- }
- }
1) DataBinding 表达式
数据绑定的布局文件和我们以前经常写的布局文件稍有不同,并从布局的根标记开始,后面依次是数据元素和视图根元素,即根布局是 layout,接下来是 data 节点,variable 节点,示例如下:
- <?xml version="1.0" encoding="utf-8"?>
- <layout xmlns:android="http://schemas.android.com/apk/res/android">
- <data>
- <variable name="user" type="com.example.User"/>
- </data>
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName}"/>
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.lastName}"/>
- </LinearLayout>
- </layout>
- <variable name="user" type="com.example.User"/>
- <span style="color:#666666;"><TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName}"/></span>
假设你现在有一个普通的 Java 对象(User):
- public class User {
- public final String firstName;
- public final String lastName;
- public User(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- }
- }
这种类型的对象具有从不改变的数据,应用程序中国通常有一次读取数据,此后不会更改,也可以使用 JavaBeans 对象:
- public class User {
- private final String firstName;
- private final String lastName;
- public User(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- }
- public String getFirstName() {
- return this.firstName;
- }
- public String getLastName() {
- return this.lastName;
- }
- }
3)数据绑定(Binding Data)
默认情况下,绑定类将根据 layout 文件的名称生成,首字母大写的命名规范,并添加 "Binding" 后缀,上述的布局文件是main_activity.xml,所以生成类是 MainActivityBinding, 该类将布局属性(例如 User 变量)的所有绑定保存到布局视图中,并知道如何为绑定表达式赋值,创建最简单的方法是在 inflating 时绑定,如下:
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
- User user = new User("Test", "User");
- binding.setUser(user);
- }
- MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
- ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
- //or
- ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
数据绑定允许你编写表达式处理,如 onClick 事件,有少数例外事件属性的名称受监听方法名称的约束,例如View.OnLongClickListener 有一个 onLongClick 方法,所以此事件的属性是:Android:onLongClick,它有两种处理事件的方法:
- 方法引用:在表达式中,可以引用符合监听方法签名的方法,当表达式计算为方法引用时,数据绑定将监听器中的方法引用和所有者对象封装在一起,并在目标视图上设置监听器,如果表达式计算为 null,则数据绑定不会创建监听器,而是设置空监听器
- 监听器绑定:这些是在事件发生时计算 Lambda 表达式,数据绑定总是创建一个监听器,它设置在视图上,当事件被发送时,监听器计算 Lambda 表达式
方法引用:事件可以直接绑定到处理程序的方法,类似于 Android:onClick,相比来看 View#onClick 属性更重要的优势是,表达式在编译的时候处理的,所以如果方法不存在或签名是不正确的,你将会在编译时出错,方法引用和监听器绑定之间的主要区别是,当数据绑定时,实际的监听器实现将创建,而不是在触发时,如果你希望事件发生时对表达式进行审核,则应该使用监听器绑定,若将事件分配给其处理程序,请使用常规绑定表达式,该值是要调用的方法名,例如,如果你的数据对象有两种方法:
- public class MyHandlers {
- public void onClickFriend(View view) { ... }
- }
注意表达式中的方法的签名必须与监听对象中的方法的签名完全匹配
- <?xml version="1.0" encoding="utf-8"?>
- <layout xmlns:android="http://schemas.android.com/apk/res/android">
- <data>
- <variable name="handlers" type="com.example.Handlers"/>
- <variable name="user" type="com.example.User"/>
- </data>
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName}"
- android:onClick="@{handlers::onClickFriend}"/>
- </LinearLayout>
- </layout>
- public class Presenter {
- public void onSaveClick(Task task){}
- }
- <?xml version="1.0" encoding="utf-8"?>
- <layout xmlns:android="http://schemas.android.com/apk/res/android">
- <data>
- <variable name="task" type="com.android.example.Task" />
- <variable name="presenter" type="com.android.example.Presenter" />
- </data>
- <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
- <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:onClick="@{() -> presenter.onSaveClick(task)}" />
- </LinearLayout>
- </layout>
监听器由 Lambda 表达式表示,这些表达式只允许作为表达式的根元素,当表达中使用回调时,数据绑定会自动创建时间的必要监听器和寄存器,当视图触发事件时,数据绑定将审核给定的表达式,与常规绑定方式一样,在审核这些监听器表达式时,任然可以得到数据绑定的 null 和线程安全性
请注意,在上面的例子中,我们还没有定义 View 的参数,通过 onClick(Android.view.View)Listener 绑定为监听器参数提供两种选择:你可以忽略所有方法参数或命名所有参数,如果你更喜欢命名参数,则也可以在表达式中使用它们,例如,上面的表达式可以写成:
- android:onClick="@{(view) -> presenter.onSaveClick(task)}"
- public class Presenter {
- public void onSaveClick(View view, Task task){}
- }
- android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
- public class Presenter {
- public void onCompletedChanged(Task task, boolean completed){}
- }
- <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
- public class Presenter {
- public boolean onLongClick(View view, Task task){}
- }
- android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
- android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
监听器表达式是非常强大的,可以使你的代码很容易阅读。另一方面,含有复杂的表达式的监听让你的布局难以阅读和维护。这些表达式应该是简单的,从用户界面传递可用数据到回调方法。您可以在从监听器表达式调用的回调方法中实现任何业务逻辑
三、布局细节(Layout Details)
1)imports
可以在数据元素内使用零个或多个导入元素。这些可以参考在你的布局文件的类,就像在 java:
- <data>
- <import type="android.view.View"/>
- </data>
现在,视图可以在绑定表达式中使用:
- <TextView
- android:text="@{user.lastName}"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
- <import type="android.view.View"/>
- <import type="com.example.real.estate.View"
- alias="Vista"/>
- <data>
- <import type="com.example.User"/>
- <import type="java.util.List"/>
- <variable name="user" type="User"/>
- <variable name="userList" type="List<User>"/>
- </data>
- <TextView
- android:text="@{((User)(user.connection)).lastName}"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- <data>
- <import type="com.example.MyStringUtils"/>
- <variable name="user" type="com.example.User"/>
- </data>
- …
- <TextView
- android:text="@{MyStringUtils.capitalize(user.lastName)}"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
2)Variables
可以在数据元素内使用任意数量的变量元素。每个变量元素描述可以在布局文件中用于绑定表达式中的布局的属性:
- <data>
- <import type="android.graphics.drawable.Drawable"/>
- <variable name="user" type="com.example.User"/>
- <variable name="image" type="Drawable"/>
- <variable name="note" type="String"/>
- </data>
在编译时检查变量类型,因此,如果变量实现 Observable 或 Observable 的集合,则该类型应反映在类型中。如果变量是不执行 Observable* 接口的基类或接口,则不会观察变量,当有不同的布局文件的各种配置(如头像),变量将被合并。这些布局文件之间不能有冲突的变量定义生成一个名为上下文的特殊变量,用于在需要时绑定表达式。上下文的值是根目录的 getcontext(),上下文变量将被一个显式变量声明所覆盖
3)自定义绑定类名(Custom Binding Class Names)
默认情况下,绑定类是基于布局的文件名生成,开始用大写,去掉下划线 "_",然后加后缀 "Binding",这个类将会被放置在一个绑定包的模块包下,例如,布局文件是 contant_item.xml 将生成 ContactItemBinding,如果模块封装为 com.example.my.app,那么它将被放置在 com.example.my.app.databinding
通过调整数据元素的类属性可以将绑定类重命名或放置在不同的包中。例如:
- <data class="ContactItem">
- ...
- </data>
- <data class=".ContactItem">
- ...
- </data>
- <data class="com.example.ContactItem">
- ...
- </data>
通过使用应用程序命名空间和属性中的变量名,可以将变量从包含布局中传递到包含布局中:
- <?xml version="1.0" encoding="utf-8"?>
- <layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:bind="http://schemas.android.com/apk/res-auto">
- <data>
- <variable name="user" type="com.example.User"/>
- </data>
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <include layout="@layout/name"
- bind:user="@{user}"/>
- <include layout="@layout/contact"
- bind:user="@{user}"/>
- </LinearLayout>
- </layout>
数据绑定不支持包括合并元素的直接子项。例如,不支持下列布局:
- <?xml version="1.0" encoding="utf-8"?>
- <layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:bind="http://schemas.android.com/apk/res-auto">
- <data>
- <variable name="user" type="com.example.User"/>
- </data>
- <merge>
- <include layout="@layout/name"
- bind:user="@{user}"/>
- <include layout="@layout/contact"
- bind:user="@{user}"/>
- </merge>
- </layout>
常用表达式给 Java 表达式很像,如下:
- Mathematical(数学)"
+ - / * %"
- String concatenation(字符串连接) "+"
- Logical(逻辑) "&& ||"
- Binary(二进制) "& | ^"
- Unary(一元运算) "+ - ! ~"
- Shift(移位) ">> >>> <<"
- Comparison(比较)"== > < >= <="
instanceof
- Grouping(分组) "()"
- Literals - character, String, numeric,
null
- Cast
- Method calls(方法调用)
- Field access
- Array access(数据访问) "[]"
- Ternary operator(三元运算) "?:"
- android:text="@{String.valueOf(index + 1)}"
- android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
- android:transitionName='@{"image_" + id}'
this
super
new
- Explicit generic invocation
- android:text="@{user.displayName ?? user.lastName}"
- android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用:
- android:text="@{user.lastName}"
- <data>
- <import type="android.util.SparseArray"/>
- <import type="java.util.Map"/>
- <import type="java.util.List"/>
- <variable name="list" type="List<String>"/>
- <variable name="sparse" type="SparseArray<String>"/>
- <variable name="map" type="Map<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]}"
- android:text='@{map["firstName"]}'
使用双引号来包含属性值也可以,字符串前需要使用"`":
- android:text="@{map[`firstName`}"
- android:text="@{map['firstName']}"
资源(Resources):
- android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式字符串和复数可通过提供参数判断:
- android:text="@{@string/nameFormat(firstName, lastName)}"
- android:text="@{@plurals/banana(bananaCount)}"
- Have an orange
- Have %d oranges
- android:text="@{@plurals/orange(orangeCount, orangeCount)}"
- 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);
- }
- }
- private static class User {
- public final ObservableField<String> firstName =
- new ObservableField<>();
- public final ObservableField<String> lastName =
- new ObservableField<>();
- public final ObservableInt age = new ObservableInt();
- }
- user.firstName.set("Google");
- int age = user.age.get();
- ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
- user.put("firstName", "Google");
- user.put("lastName", "Inc.");
- user.put("age", 17);
- <data>
- <import type="android.databinding.ObservableMap"/>
- <variable name="user" type="ObservableMap<String, Object>"/>
- </data>
- …
- <TextView
- android:text='@{user["lastName"]}'
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- <TextView
- android:text='@{String.valueOf(1 + (Integer)user["age"])}'
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- ObservableArrayList<Object> user = new ObservableArrayList<>();
- user.add("Google");
- user.add("Inc.");
- user.add(17);
- <data>
- <import type="android.databinding.ObservableList"/>
- <import type="com.example.my.app.Fields"/>
- <variable name="user" type="ObservableList<Object>"/>
- </data>
- …
- <TextView
- android:text='@{user[Fields.LAST_NAME]}'
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- <TextView
- android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
五、Binding 生成(Generated Binding)
- MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
- MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
- MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时 Binding 不能事先知道,在这种情况下,可以使用 DataBindingUtil 类来创建 Binding:
- ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
- parent, attachToParent);
- ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
- <layout xmlns:android="http://schemas.android.com/apk/res/android">
- <data>
- <variable name="user" type="com.example.User"/>
- </data>
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName}"
- android:id="@+id/firstName"/>
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.lastName}"
- android:id="@+id/lastName"/>
- </LinearLayout>
- </layout>
- public final TextView firstName;
- public final TextView lastName;
IDS 几乎没有必要在 DataBinding,但仍然有一些情况下,访问 Views 仍然是必要的代码
- <data>
- <import type="android.graphics.drawable.Drawable"/>
- <variable name="user" type="com.example.User"/>
- <variable name="image" type="Drawable"/>
- <variable name="note" type="String"/>
- </data>
- public abstract com.example.User getUser();
- public abstract void setUser(com.example.User user);
- public abstract Drawable getImage();
- public abstract void setImage(Drawable image);
- public abstract String getNote();
- public abstract void setNote(String note);
- public void onBindViewHolder(BindingHolder holder, int position) {
- final T item = mItems.get(position);
- holder.getBinding().setVariable(BR.item, item);
- holder.getBinding().executePendingBindings();
- }
直接 Binding
- <android.support.v4.widget.DrawerLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:scrimColor="@{@color/scrim}"
- app:drawerListener="@{fragment.drawerListener}"/>
重命名 Setters(Renamed Setters)
- @BindingMethods({
- @BindingMethod(type = "android.widget.ImageView",
- attribute = "android:tint",
- method = "setImageTintList"),
- })
- @BindingAdapter("android:paddingLeft")
- public static void setPaddingLeft(View view, int padding) {
- view.setPadding(padding,
- view.getPaddingTop(),
- view.getPaddingRight(),
- view.getPaddingBottom());
- }
你也可以创建可以接收多个参数的适配器:
- @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 app:imageUrl="@{venue.imageUrl}"
- app:error="@{@drawable/venueError}"/>
如果对于一个 ImageView imageUrl 和 error 都被使用,并且 imageUrl 是一个 String 类型以及 error 是一个 drawable 时,该适配器被调用
- 匹配的过程中自定义 name spaces 将被忽略
- 你也可以为 Android name spaces 写适配器
- @BindingAdapter("android:paddingLeft")
- public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
- if (oldPadding != newPadding) {
- view.setPadding(newPadding,
- view.getPaddingTop(),
- view.getPaddingRight(),
- view.getPaddingBottom());
- }
- }
事件处理程序只能用一个抽象方法与接口或抽象类一起使用,例如:
- @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);
- }
- }
- }
当监听器有多个方法时,它必须被分割成多个监听器,例如,View.OnAttachStateChangeListener 的方法有两种:onViewAttachedToWindow() 和 onViewDetachedFromWindow()。然后,我们必须创建两个接口来区分它们的属性和处理程序:
- @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
- public interface OnViewDetachedFromWindow {
- void onViewDetachedFromWindow(View v);
- }
- @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
- public interface OnViewAttachedToWindow {
- void onViewAttachedToWindow(View v);
- }
因为改变一个监听器也会影响另一个,我们必须有三个不同的绑定适配器,一个为每个属性和一个为两者,他们应该被设置:
- @BindingAdapter("android:onViewAttachedToWindow")
- public static void setListener(View view, OnViewAttachedToWindow attached) {
- setListener(view, null, attached);
- }
- @BindingAdapter("android:onViewDetachedFromWindow")
- public static void setListener(View view, OnViewDetachedFromWindow detached) {
- setListener(view, detached, null);
- }
- @BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
- public static void setListener(View view, final OnViewDetachedFromWindow detach,
- final OnViewAttachedToWindow attach) {
- if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
- final OnAttachStateChangeListener newListener;
- if (detach == null && attach == null) {
- newListener = null;
- } else {
- newListener = new OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- if (attach != null) {
- attach.onViewAttachedToWindow(v);
- }
- }
- @Override
- public void onViewDetachedFromWindow(View v) {
- if (detach != null) {
- detach.onViewDetachedFromWindow(v);
- }
- }
- };
- }
- final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
- newListener, R.id.onAttachStateChangeListener);
- if (oldListener != null) {
- view.removeOnAttachStateChangeListener(oldListener);
- }
- if (newListener != null) {
- view.addOnAttachStateChangeListener(newListener);
- }
- }
- }
上面的例子是比正常的稍微复杂,因为 View 使用添加和删除的监听者而不是为 View.OnAttachStateChangeListener 设置方法,android.databinding.adapters.listenerutil 类有助于保持跟踪,他们可能会在绑定适配器删除以前的 listener
- <TextView
- android:text='@{userMap["lastName"]}'
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- <View
- android:background="@{isError ? @color/red : @color/white}"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
这里,背景需要 Drawable 对象,但颜色是一个整数,不管何时有 Drawable 并且返回值是一个整数,那么整数类型会被转换为ColorDrawable 看,这个转换是通过使用带带有 BindingConversion 注解的静态方法完成的:
- @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"/>
- 语法高亮
- 表达式语言语法错误的标记
- XML 代码完成
- 引用和快速文档
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName, default=PLACEHOLDER}"/>
如果你需要你的项目的设计阶段中显示一个默认值,你也可以使用工具的属性而不是默认的表达式的值,在设计 layout 描述属性