Android Data Binding 系列 -- 详细介绍与使用
Posted ConnorLin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Data Binding 系列 -- 详细介绍与使用相关的知识,希望对你有一定的参考价值。
写在前面
要学习新东西,最好的办法是先学会如何使用。所以,本文仅作 android Data Binding 的介绍并结合 DataBindingDemo 来理解它的用法,后续再对其原理进行深入探讨。
简介
Data binding 在2015年7月发布的Android Studio v1.3.0 版本上引入,在2016年4月Android Studio v2.0.0 上正式支持。目前为止,Data Binding 已经支持双向绑定了。
Databinding 是一个实现数据和UI绑定的框架,是一个实现 MVVM 模式的工具,有了 Data Binding,在Android中也可以很方便的实现MVVM开发模式。
Data Binding 是一个support库,最低支持到Android 2.1(API Level 7+)。
Data Binding 之前,我们不可避免地要编写大量的毫无营养的代码,如 findViewById()、setText(),setVisibility(),setEnabled() 或 setOnClickListener() 等,通过 Data Binding , 我们可以通过声明式布局以精简的代码来绑定应用程序逻辑和布局,这样就不用编写大量的毫无营养的代码了。
构建环境
首先,确保能使用Data Binding,需要下载最新的 Support repository。否则可能报错,如图:
在模块的build.gradle文件中添加dataBinding配置
android .... dataBinding enabled = true
注意:如果app依赖了一个使用 Data Binding 的库,那么app module 的 build.gradle 也必须配置 Data Binding。
Data Binding 布局文件 - (View)
Data binding 的布局文件与传统布局文件有一点不同。它以一个 layout 标签作为根节点,里面是 data 标签与 view 标签。view 标签的内容就是不使用 Data Binding 时的普通布局文件内容。以下是一个例子:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<!-- 变量user, 描述了一个布局中会用到的属性 -->
<variable name="user" type="com.connorlin.databinding.model.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>
数据对象 - (Model)
假设你有一个 plain-old Java object(POJO) 的 User 对象。
public class User
private final String mFirstName;
private final String mLastName;
private int mAge;
public User(String firstName, String lastName, int age)
mFirstName = firstName;
mLastName = lastName;
mAge = age;
或者是 JavaBean 对象:
public class User
private final String mFirstName;
private final String mLastName;
private int mAge;
public User(String firstName, String lastName, int age)
mFirstName = firstName;
mLastName = lastName;
mAge = age;
public String getFirstName()
return mFirstName;
public String getLastName()
return mLastName;
public int getAge()
return mAge;
从 Data Binding 的角度看,这两个类是一样的。用于 TextView 的 android:text
属性的表达式@user.firstName
,会读取 POJO 对象的 firstName
字段以及 JavaBeans 对象的 getFirstName()
方法。
绑定数据 - (ViewModel)
在默认情况下,会基于布局文件生成一个继承于 ViewDataBinding
的 Binding 类,将它转换成帕斯卡命名并在名字后面接上Binding
。例如,布局文件叫 main_activity.xml
,所以会生成一个 MainActivityBinding
类。这个类包含了布局文件中所有的绑定关系,会根据绑定表达式给布局文件赋值。在 inflate 的时候创建 binding 的方法如下:
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
// ActivityBaseBinding 类是自动生成的
ActivityBaseBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_base);
User user = new User("Connor", "Lin");
// 所有的 set 方法也是根据布局中 variable 名称生成的
binding.setUser(user);
事件处理
本部分源码请参考 DataBindingDemo -> EventActivity
部分。
类似于 android:onClick 可以指定 Activity 中的函数,Data Binding 也允许处理从视图中发送的事件。
有两种实现方式:
- 方法调用
- 监听绑定
二者主要区别在于方法调用在编译时处理,而监听绑定于事件发生时处理。
方法调用
相较于 android:onClick ,它的优势在于表达式会在编译时处理,如果函数不存在或者函数签名不对,编译将会报错。
以下是个例子:
public class EventHandler
private Context mContext;
public EventHandler(Context context)
mContext = context;
public void onClickFriend(View view)
Toast.makeText(mContext, "onClickFriend", Toast.LENGTH_LONG).show();
表达式如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handler"
type="com.connorlin.databinding.handler.EventHandler"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@handler::onClickFriend"/>
<!-- 注意:函数名和监听器对象必须对应 -->
<!-- 函数调用也可以使用 `.` , 如handler.onClickFriend , 不过已弃用 -->
</LinearLayout>
</layout>
监听绑定
监听绑定在事件发生时调用,可以使用任意表达式
此功能在 Android Gradle Plugin version 2.0 或更新版本上可用.
在方法引用中,方法的参数必须与监听器对象的参数相匹配。在监听绑定中,只要返回值与监听器对象的预期返回值相匹配即可。
以下是个例子:
public void onTaskClick(Task task)
task.run();
表达式如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handler" type="com.connorlin.databinding.handler.EventHandler"/>
<variable
name="task" type="com.connorlin.databinding.task.Task"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@() -> handler.onTaskClick(task)"/>
</LinearLayout>
</layout>
当一个回调函数在表达式中使用时,数据绑定会自动为事件创建必要的监听器并注册监听。
关于参数
- 参数有两种选择:要么不写,要么就要写全。
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@() -> handler.onTaskClick(task)" />
或
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@(view) -> handler.onTaskClick(task)"/>
- lambda 表达式可添加一个或多个参数,同时参数可任意命名
public class EventHandler
public void onTaskClickWithParams(View view, Task task)
task.run();
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@(theview) -> handler.onTaskClickWithParams(theview, task)" />
或者
public class EventHandler
public void onCompletedChanged(Task task, boolean completed)
if(completed)
task.run();
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@(cb, isChecked) -> handler.onCompletedChanged(task, isChecked)" />
表达式结果有默认值 null、0、false等等
表达式中可以使用void
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@(v) -> v.isVisible() ? doSomething() : void" />
关于表达式
复杂的表达式会使布局难以阅读和维护,这种情况我们最好将业务逻辑写到回调函数中
也有一些特殊的点击事件 我们需要使用不同于 android:onClick 的属性来避免冲突。
下面是一些用来避免冲突的属性:
Class | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
布局详情
本部分源码请参考 DataBindingDemo -> CombineActivity
部分
导入(Imports)
- data 标签内可以有多个 import 标签。你可以在布局文件中像使用 Java 一样导入引用
<data>
<import type="android.view.View"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@user.isAdult ? View.VISIBLE : View.GONE"/>
- 当类名发生冲突时,可以使用 alias
<import type="android.view.View"/>
<import type="com.connorlin.databinding.ui.View" alias="AliasView"/>
- 导入的类型也可以用于变量的类型引用和表达式中
<data>
<import type="com.connorlin.databinding.model.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
注意:Android Studio 还没有对导入提供自动补全的支持。你的应用还是可以被正常编译,要解决这个问题,你可以在变量定义中使用完整的包名。
- 导入也可以用于在表达式中使用静态方法
public class MyStringUtils
public static String capitalize(final String word)
if (word.length() > 1)
return String.valueOf(word.charAt(0)).toUpperCase() + word.substring(1);
return word;
<data>
<import type="com.connorlin.databinding.utils.MyStringUtils"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
</data>
…
<TextView
android:text="@MyStringUtils.capitalize(user.lastName)"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
- java.lang.* 包中的类会被自动导入,可以直接使用,例如, 要定义一个 String 类型的变量
<variable name="test" type="String" />
变量 Variables
- data 标签中可以有任意数量的 variable 标签。每个 variable 标签描述了会在 binding 表达式中使用的属性。
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
- 可以在表达式中直接引用带 id 的 view,引用时采用驼峰命名法。
<TextView
android:id="@+id/first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@=user.firstName" />
<TextView
android:text="@user.lastName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@firstName.getVisibility() == View.GONE ? View.GONE : View.VISIBLE" />
<!-- 这里TextView直接引用第一次TextView,firstName为id 的驼峰命名 -->
- binding 类会生成一个命名为 context 的特殊变量(其实就是 rootView 的 getContext() ) 的返回值),这个变量可用于表达式中。 如果有名为 context 的变量存在,那么生成的这个 context 特殊变量将被覆盖。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@handler.loadString(context)"/>
public String loadString(Context context)
// 使用生成的context变量
return context.getResources().getString(R.string.string_from_context);
自定义绑定类名
默认情况下,binding 类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加 “Binding” 结尾。这个类会被放置在 databinding 包中。举个例子,布局文件 contact_item.xml 会生成 ContactItemBinding 类。如果 module 包名为 com.example.my.app ,binding 类会被放在 com.example.my.app.databinding 中。
通过修改 data 标签中的 class 属性,可以修改 Binding 类的命名与位置。举个例子:
<data class="CustomBinding">
...
</data>
以上会在 databinding 包中生成名为 CustomBinding 的 binding 类。如果需要放置在不同的包下,可以在前面加 “.”
:
<data class=".CustomBinding">
...
</data>
这样的话, CustomBinding 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中:
<data class="com.example.CustomBinding">
...
</data>
Includes
在使用应用命名空间的布局中,变量可以传递到任何 include
布局中。
<?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.connorlin.databinding.model.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/include"
app:user="@user"/>
</LinearLayout>
</layout>
需要注意, activity_combine.xml 与 include.xml 中都需要声明 user 变量。
Data binding 不支持直接包含 merge
节点。举个例子, 以下的代码不能正常运行 :
<?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.connorlin.databinding.model.User"/>
</data>
<merge>
<include layout="@layout/include"
app:user="@user"/>
</merge>
</layout>
表达式语言
通用特性
表达式语言与 Java 表达式有很多相似之处。下面是相同之处:
- 数学计算 + - / * %
- 字符串连接 +
- 逻辑 && ||
- 二进制 & | ^
- 一元 + - ! ~
- 位移 >> >>> <<
- 比较 == > < >= <=
- instanceof
- 组 ()
- 字面量 - 字符,字符串,数字, null
- 类型转换
- 函数调用
- 字段存取
- 数组存取 []
- 三元运算符 ?:
例子:
<!-- 内部使用字符串 & 字符拼接-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@`Age :` + String.valueOf(user.age)"/>
<!-- 三目运算-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@user.isAdult ? View.VISIBLE : View.GONE"/>
在xml中转义是不可避免的,如 : 使用“&&”是编译不通过的,需要使用转义字符 “&&”
附:常用的转义字符
显示结果 | 描述 | 转义字符 | 十进制 |
---|---|---|---|
空格 | |   | |
< | 小于号 | < | < |
> | 大于号 | > | > |
& | 与号 | & | & |
" | 引号 | " | " |
‘ | 撇号 | ' | ' |
× | 乘号 | × | × |
÷ | 除号 | ÷ | ÷ |
不支持的操作符
一些 Java 中的操作符在表达式语法中不能使用。
- this
- super
- new
- 显式泛型调用
<T>
Null合并运算符
Null合并运算符 ??
会在非 null 的时候选择左边的操作,反之选择右边。
android:text="@user.lastName ?? `Default LastName`"
等同于
android:text="@user.lastName != null ? user.lastName : `Default LastName`"
容器类
通用的容器类:数组,lists,sparse lists,和 maps,可以用 []
操作符来存取
<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"]"
资源
也可以在表达式中使用普通的语法来引用资源:
android:text="@@string/fullname(user.fullName)"
字符串格式化和复数形式可以这样实现:
android:text="@@plurals/sample_plurals(num)"
当复数形式有多个参数时,应该这样写:
android:text="@@plurals/numbers(num, num)"
一些资源需要显示类型调用。
Type | Normal Reference | Expression Reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
数据对象 (Data Objects)
任何 POJO 对象都能用在 Data Binding 中,但是更改 POJO 并不会同步更新 UI。Data Binding 的强大之处就在于它可以让你的数据拥有更新通知的能力。
有三种不同的动态更新数据的机制:
- Observable 对象
- Observable 字段
- Observable 容器类
当以上的 observable 对象绑定在 UI 上,数据发生变化时,UI 就会同步更新。
Observable 对象
当一个类实现了 Observable 接口时,Data Binding 会设置一个 listener 在绑定的对象上,以便监听对象字段的变动。
Observable 接口有一个添加/移除 listener 的机制,但通知取决于开发者。为了简化开发,Android 原生提供了一个基类 BaseObservable
来实现 listener 注册机制。这个类也实现了字段变动的通知,只需要在 getter 上使用 Bindable 注解,并在 setter 中通知更新即可。
public class ObservableContact extends BaseObservable
private String mName;
private String mPhone;
public ObservableContact(String name, String phone)
mName = name;
mPhone = phone;
@Bindable
public String getName()
return mName;
public void setName(String name)
mName = name;
notifyPropertyChanged(BR.name);
@Bindable
public String getPhone()
return mPhone;
public void setPhone(String phone)
mPhone = phone;
notifyPropertyChanged(BR.phone);
BR
是编译阶段生成的一个类,功能与 R.java 类似,用 @Bindable 标记过 getter 方法会在 BR 中生成一个 entry。
当数据发生变化时需要调用 notifyPropertyChanged(BR.firstName)
通知系统 BR.firstName
这个 entry 的数据已经发生变化以更新UI。
ObservableFields
创建 Observable 类还是需要花费一点时间的,如果想要省时,或者数据类的字段很少的话,可以使用 ObservableField
以及它的派生 ObservableBoolean
、
ObservableByte
、ObservableChar
、ObservableShort
、ObservableInt
、ObservableLong
、ObservableFloat
、ObservableDouble
、
ObservableParcelable
。
ObservableFields 是包含 observable 对象的单一字段。原始版本避免了在存取过程中做打包/解包操作。要使用它,在数据类中创建一个 public final 字段:
public class ObservableFieldContact
public ObservableField<String> mName = new ObservableField<>();
public ObservableField<String> mPhone = new ObservableField<>();
public ObservableFieldContact(String name, String phone)
mName.set(name);
mPhone.set(phone);
要存取数据,只需要使用 get() / set() 方法:
mObservableFieldContact.mName.set("ConnorLin");
mObservableFieldContact.mPhone.set("12345678901");
String name = mObservableFieldContact.mName.get();
Observable Collections 容器类
一些应用会使用更加灵活的结构来保持数据。Observable 容器类允许使用 key 来获取这类数据。当 key 是类似 String 的一类引用类型时,使用 ObservableArrayMap 会非常方便。
ObservableArrayMap<String, String> mUser = new ObservableArrayMap<>();
mUser.put("firstName", "Connor");
mUser.put("lastName", "Lin");
mUser.put("age", "28");
mBinding.setUser(mUser);
在布局中,可以用 String key 来获取 map 中的数据:
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, String>"/>
</data>
…
<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"]'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@user["age"]'/>
当 key 是整数类型时,可以使用 ObservableArrayList :
ObservableArrayList<String> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add("17");
在布局文件中,使用下标获取列表数据:
<data>
<import type="android.databinding.ObservableList"/>
<variable name="user" type="ObservableList<String>"/>
</data>
…
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@userList[0]'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@userList[1]'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@userList[2]'/>
生成绑定
生成的 binding 类将布局中的 View 与变量绑定在一起。就像先前提到过的,类名和包名可以自定义 。生成的 binding 类会继承 ViewDataBinding 。
Creating
binding 应该在 inflate 之后创建,确保 View 的层次结构不会在绑定前被干扰。绑定布局有好几种方式。最常见的是使用 binding 类中的静态方法。inflate 函数会 inflate View 并将 View 绑定到 binding 类上。此外有更加简单的函数,只需要一个 LayoutInflater 或一个 ViewGroup:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果布局使用不同的机制来 inflate,则可以独立做绑定操作:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时绑定关系是不能提前确定的。这种情况下,可以使用 DataBindingUtil :
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
Views With IDs
布局中每一个带有 ID 的 View,都会生成一个 public final 字段。binding 过程会做一个简单的赋值,在 binding 类中保存对应 ID 的 View。这种机制相比调用 findViewById 效率更高。举个例子:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.connorlin.databinding.model.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>
将会在 binding 类内生成:
public final TextView firstName;
public final TextView lastName;
ID 在 Data Binding 中并不是必需的,但是在某些情况下还是有必要对 View 进行操作。
Variables
每一个变量会有相应的存取函数:
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
并在 binding 类中生成对应的 getters 和 setters:
public com.connorlin.databinding.model.User getUser();
public void setUser(com.connorlin.databinding.model.User user);
public Drawable getImage();
public void setImage(Drawable image);
public String getNote();
public void setNote(String note);
ViewStubs
本部分源码请参考 DataBindingDemo -> ViewStubActivity
部分。
ViewStub 相比普通 View 有一些不同。ViewStub 一开始是不可见的,当它们被设置为可见,或者调用 inflate 方法时,ViewStub 会被替换成另外一个布局。
因为 ViewStub 实际上不存在于 View 结构中,binding 类中的类也得移除掉,以便系统回收。因为 binding 类中的 View 都是 final 的,所以Android 提供了一个叫 ViewStubProxy
的类来代替 ViewStub 。开发者可以使用它来操作 ViewStub,获取 ViewStub inflate 时得到的视图。
但 inflate 一个新的布局时,必须为新的布局创建一个 binding。因此, ViewStubProxy
必须监听 ViewStub 的 ViewStub.OnInflateListener
,并及时建立 binding。由于 ViewStub 只能有一个 OnInflateListener
,你可以将你自己的 listener 设置在 ViewStubProxy 上,在 binding 建立之后, listener 就会被触发。
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout ...>
<ViewStub
android:id="@+id/view_stub"
<以上是关于Android Data Binding 系列 -- 详细介绍与使用的主要内容,如果未能解决你的问题,请参考以下文章
Android Data Binding 系列 -- 详细介绍与使用
Android Data Binding 系列 -- 详细介绍与使用
WPF QuickStart系列之数据绑定(Data Binding)