文章导航
Android Data Binding数据绑定详解(基础篇)
Android Data Binding数据绑定详解(进阶篇)
简介
在2015年的谷歌IO大会上,Android UI Toolkit团队发布了DataBinding 框架,将数据绑定引入了Android开发。以后可以直接在 layout 布局 xml 文件中绑定数据了,无需再 findViewById 然后手工设置数据了。官方文档说明
构建环境
使用之前,需要在Android Studio中build.gradle配置开启Data Binding,具体如下:
android {
....
dataBinding {
enabled = true
}
}
Data Binding Layout文件
1.Data Binding表达式
Data Binding layout文件有点不同的是:起始根标签是layout,接下来一个data元素以及一个view的根元素。这个view元素就是你没有使用Data Binding的layout文件的根元素。举例说明如下:
<?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>
在data内type对应的是实体类User,name是实体类的实例。在布局中可以直接通过 @{} 绑定属性值。
2.Data对象
假设你有一个user的plain-old Java Object(POJO):
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
这个类型的对象拥有从不改变的数据。在app中它是常见的,可以读取一次并且之后从不改变。当然也可以使用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;
}
}
从Data Binding的角度来看,这两个类是等价的。用于TextView中的android:text属性的表达式 @{user.firstName} 将访问前者POJO对象中的firstName 和后者JavaBeans对象中的getFirstName() 方法。
在Android中接收后台返回实体类,多少难免会有一些空值,而我们在不注意的时候直接使用则会引发空指针这个异常,那么使用Data Binding后,我们则无需关注这块内容。如果使用Data Binding,如果数据出现空时,则默认显示当前类型的默认值。
3.Binding数据
默认情况下,一个Binding类会基于layout文件的名称而产生,将其转换为Pascal case(译注:首字母大写的命名规范)并且添加“Binding”后缀。上述的layout文件是main_activity.xml,因此生成的类名是MainActivityBinding。此类包含从layout属性到layout的Views中所有的bindings(例如user变量),并且它还知道如何给Binding表达式分配数值。
创建bindings的最简单的方式是在inflating(译注:layout文件与Activity/Fragment的“链接”)期间如下:
@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);
}
或者你可以通过如下获取View:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
在ListView 或者RecyclerView adapter 使用Data Binding时,
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup,
false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
4.事件处理
4.1 在Activity中定义事件类以及进行初始化:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityBindListenerBinding mBindListenerBinding = DataBindingUtil.setContentView(this, R.layout.activity_bind_listener);
mBindListenerBinding.setPersenter(new Presenter());
}
初始化这个事件内部类,方式同样有俩种,如下:
- mBindListenerBinding.setPersenter(new Presenter());
- mBindListenerBinding.setVariable(BR.persenter,new Persenter());
4.2 布局文件引入命名空间,进行相关调用:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.hlq.databindingdemo.bean.UserBean" />
<variable
name="persenter"
type="com.hlq.databindingdemo.activity.Presenter" />
</data>
<android.support.v7.widget.LinearLayoutCompat xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="15dp"
tools:context="com.hlq.databindingdemo.activity.BindListenerActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{persenter.onClick}"
android:text="测试监听器绑定." />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{persenter::onClick}"
android:text="测试监听器绑定:" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> persenter.getUserClick()}"
android:text="测试方法绑定" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> persenter.showUserName(user)}"
android:text="测试方法绑定传值" />
</android.support.v7.widget.LinearLayoutCompat>
</layout>
其中,调用事件的方式有俩种,分别如下:
- android:onClick="@{persenter.onClick}"
- android:onClick="@{persenter::onClick}" 官方更为推崇使用这种。
而Lambda 表达式使用方式如下:
- android:onClick="@{() -> persenter.getUserClick()}"
- android:onClick="@{() -> persenter.showUserName(user)}"
4.3 Activity中的内部类Presenter(也可以抽取出去)
public class Presenter {
public void onClick(View view) {
}
public void getUserClick() {
}
public void showUserName(UserBean userBean) {
}
}
5.深入Layout文件
5.1 Import
import 元素允许你的布局文件引用类,就像在java文件中引用一样:
<data>
<import type="android.view.View"/>
</data>
现在,View 就可以在表达式中使用了(例如, View.VISIBLE):
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
当类名有冲突时,其中一个类名可以重命名为alias,在该layout文件中Vista 对应com.example.real.estate.View,而View对应android.view.View
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
导入的类型可以在Variable和表达式中使用作为引用来使用:
注意:Android Studio还没有处理imports,所以自动导入Variable在你的IDE不能使用。您的app仍会正常编译,你可以在您的Variable定义中使用完全符合规定的名称来解决该IDE问题。
<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"/>
导入的类型还可以在表达式中使用static属性和方法:
<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"/>
5.2 自定义Binding类名称
默认情况下,Binding类的命名是基于所述layout文件的名称,用大写开头,除去下划线()以及()后的第一个字母大写,然后添加“Binding”后缀。这个类将被放置在一个模块封装包里的databinding封装包下。例如,所述layout文件contact_item.xml将生成ContactItemBinding。如果模块包是com.example.my.app,那么它将被放置在com.example.my.app.databinding。
Binding类可通过调整data元素中的class属性来重命名或放置在不同的包中。例如:
<data class="ContactItem">
...
</data>
在模块封装包的databinding包中会生成名为ContactItem的Binding类。如果要想让该类生成在不同的包种,你需要添加前缀.,如下:
<data class=".ContactItem">
...
</data>
在这个情况下,ContactItem类直接在模块包种生成。或者你可以提供整个包名:
<data class="com.example.ContactItem">
...
</data>
5.3 Includes
首先,编写一个layout,由于我们需要将数据源也传递过去,所以这里需要提前声明下实体。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.hlq.databindingdemo.bean.UserBean" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
style="@style/titleStyle"
android:text='@{"姓名:"+user.userName}' />
<TextView
style="@style/titleStyle"
android:text='@{"年龄:"+user.userAge}' />
<TextView
style="@style/titleStyle"
android:text='@{"地址:"+user.userAddress}' />
</LinearLayout>
</layout>
在所对应的Activity layout中通过bind绑定数据源即可:
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.hlq.databindingdemo.bean.UserBean" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.hlq.databindingdemo.activity.IncludeActivity">
<TextView
style="@style/contentStyle"
android:text="include引用布局示例" />
<include
layout="@layout/include_item_layout"
bind:user="@{user}" />
</LinearLayout>
</layout>
Activity中初始化数据源:
ActivityIncludeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_include);
binding.setUser(new UserBean("静心Study", "22", "帝都"));
注意:绑定数据不支持 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.example.User"/>
</data>
<merge>
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>
5.4 ViewStub
首先编写一个viewStub的layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
style="@style/contentStyle"
android:text="今天上班了" />
<TextView
style="@style/titleStyle"
android:text="难受想哭,心里默默MMP" />
</LinearLayout>
编写布局:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.hlq.databindingdemo.activity.ViewStubActivity">
<TextView
style="@style/contentStyle"
android:text="ViewStub示例" />
<ViewStub
android:id="@+id/viewStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_view_stub" />
</LinearLayout>
</layout>
Activity进行初始化:
ActivityViewStubBinding viewStubBinding =
DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
viewStubBinding.viewStub.getViewStub().inflate();
5.5 表达式
(a)常用表达式跟Java表达式很像,以下这些是一样的:
- 数学 + - / * %
- 字符串连接 +
- 逻辑 && ||
- 二进制 & | ^
- 一元运算 + - ! ~
- 移位 >> >>> <<
- 比较 == > < >= <=
- instanceof
- 分组 ()
- null
- Cast
- 方法调用
- 数据访问 []
- 三元运算 ?:
示例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
(b)缺少的操作:
- this
- super
- new
- 显式泛型调用
(c)Null合并操作
?? - 左边的对象如果它不是null,选择左边的对象;或者如果它是null,选择右边的对象: android:text="@{user.displayName ?? user.lastName}"
(d)集合
常用的集合:arrays、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]}"
(e)字符串
当使用单引号包含属性值时,在表达式中使用双引号很容易:
android:text='@{map["firstName"]}'
使用双引号来包含属性值也是可以的。字符串前后需要使用“`”:
android:text="@{map[`firstName`]}"
android:text="@{map["firstName"]}"
(f)Resources
使用正常的表达式来访问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)}"
下面一些资源的显示调用跟普通的调用略有不同:
类型 | 正常引用 | 表达式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
5.6 Data 对象
任何Plain old Java object(POJO)可用于Data Binding,但修改POJO不会导致UI更新。Data Binding的真正能力是当数据变化时,可以通知给你的Data对象。有三种不同的数据变化通知机制:Observable对象、ObservableFields以及observable collections。
当这些可观察Data对象绑定到UI,Data对象属性的更改后,UI也将自动更新。
(a)Observable 对象
实现android.databinding.Observable接口的类可以允许附加一个监听器到Bound对象以便监听对象上的所有属性的变化。
Observable接口有一个机制来添加和删除监听器,但通知与否由开发人员管理。为了使开发更容易,一个BaseObservable的基类为实现监听器注册机制而创建。
通过指定一个Bindable注解给getter;
在setter内通过notifyPropertyChanged(BR.classNo)设置更新。
public class ClassBean extends BaseObservable {
private String classNo;
private String classNum;
private String className;
public ClassBean(String className) {
this.className = className;
}
public ClassBean(String className, String classNum) {
this.className = className;
this.classNum = classNum;
}
public ClassBean(String classNo, String classNum, String className) {
this.classNo = classNo;
this.classNum = classNum;
this.className = className;
}
@Bindable
public String getClassNo() {
return classNo;
}
@Bindable
public String getClassName() {
return className;
}
@Bindable
public String getClassNum() {
return classNum;
}
public void setClassNo(String classNo) {
this.classNo = classNo;
notifyPropertyChanged(BR.classNo);
}
public void setClassName(String className) {
this.className = className;
notifyPropertyChanged(BR.classNo);
}
public void setClassNum(String classNum) {
this.classNum = classNum;
notifyPropertyChanged(BR.classNo);
}
}
动态修改实体,从而进行UI即时刷新。
private ActivityObservableBinding mBinding;
private ClassBean mClassBean = new ClassBean("001", "100", "A1T105");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_observable);
mBinding.setClassX(mClassBean);
mBinding.setPresenter(new Presenter());
}
//修改实体,UI界面即使刷新
public class Presenter {
public void onTextChanged(CharSequence s, int start, int before, int count) {
mClassBean.setClassNo("No:" + s);
mClassBean.setClassNum("Num:" + s);
mClassBean.setClassName("Name:" + s);
}
}
-
notifyPropertyChanged(BR.classNo);
用于设置单个更新字段,希望哪个字段更新就给哪个字段设置 -
notifyChange();
用于更新所有字段,只需要给一个字段设置,所有的字段都会更新
(b)Observable 字段
ObservableFields是自包含具有单个字段的observable对象。
Step 1: 定义实体类
public class MissBean {
public final ObservableField<String> missWho = new ObservableField<>();
public final ObservableField<String> missYou = new ObservableField<>();
}
Step 2: 布局引用
引入命名空间。
<data>
<variable
name="missBean"
type="com.hlq.databindingdemo.bean.MissBean" />
</data>
设置对应值。
<TextView
style="@style/titleStyle"
android:text="@{missBean.missWho}" />
<TextView
style="@style/titleStyle"
android:text="@{missBean.missYou}" />
Step 3: Activity中初始化 - 赋值
mMissBean = new MissBean();
mMissBean.missWho.set("在思念谁?");
mMissBean.missYou.set("在思念你~");
//取值
//mMissBean.missYou.get();
mBinding.setMissBean(mMissBean);
(c)Observable 集合
ObservableArrayList 用于键是整数
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在layout文件中,通过索引可以访问list:
<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"/>
这里需要注意,指定type时,ObservableArrayList类型里面不能包含<,而是通过 < ; 代替。
ObservableArrayMap用于键是引用类型,如String。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在layout文件中,通过String键可以访问map:
<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"/>