Data Binding 学习笔记
在Android系统中使用Java编写逻辑代码,而使用XML表示界面View,从而使得控制逻辑与显示界面分离,实现一定程度的解耦。但是,界面显示部分与控制逻辑还是有一定的数据和事件的联系,在通常的编码中是在Java逻辑代码中获取XML中的控件,并在代码中为其设置属性以及事件监听器等方式实现控制界面View显示的目的。
Data Binding则是通过在XML文件中引入对象变量,使用变量控制XML各个控件的属性以及事件,并在代码中为每个XML文件生成ViewDataBinding对象,从而实现代码对XML的更好控制。逻辑对界面的控制无非包括控件属性和控件响应事件两个方面,分别对应着数据和事件两个方面,下面从数据绑定,事件绑定以及双向数据绑定三个方面介绍Data Binding的相关内容。
数据绑定
首先介绍一下DataBinding的基本用法,在Android Studio 1.3之后,可以在module的build.gradle文件中直接进行如下配置:
//module的build.gradle
android {
....
dataBinding {
enabled = true
}
}
Data Binding 是在XML文件中引入对象变量,XML文件的形式有所改变,下面为官方文档中的实例:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.recluseguo.recluseandroid.data.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>
XML 文件根标签为,在根标签内部包括两个子标签,和原来界面XML根标签。在标签中可以使用引入变量,name属性是在该XML文件中该变量的名称,type则为该变量所对应Java类的全限定名。在原来的界面XML文件中,可以通过@{}的方式使用该变量,大括号内为变量的引入。在标签内部还有另外两个可用子标签本文后面细节部分会有介绍。
在XML文件中使用的对象变量所属的类的定义如下:
package com.recluseguo.recluseandroid.data;
public class User {
private String firstName;
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
Data Binding 使用变量对象获取对象属性从而设置控件属性时可以通过两种方法,一是本例中采用的getter方法,另外一种就是讲属性设置为public也可以实现对对象属性的访问。
最后就是在Android的控制逻辑Java代码中获取XML对应的ViewDataBinding对象,Data Binding Library会为XML文件生成一个类,该类实例化的对象可以用在Java代码中控制界面中中的View,类的名称由XML的文件名决定,如activity_main.xml文件生成的类为ActivityMainBinding,这个类是继承自ViewDataBinding。该类会为在标签里的每一个声明的变量添加getter和setter方法,因此可以控制XML中的界面属性。实例化这个类的对象的方法有两种方式:
//使用生成类的静态方法inflate()
ActivityMainBinding binding = ActivityMainBinding.inflate(layoutInflator, viewGroup, isAttach);
//使用DataBindingUtil的静态方法inflate()
ActivityMainBinding binding = DataBindingUtil.inflate(layoutInflator, R.layout.activity_main, viewGroup, isAttach);
在Fragment,ListView以及RecyclerView中,使用DataBinding可以通过此方法获取binding对象,然后通过binding.getRoot()方法获得根View即可,而在activity中封装了setContentView()方法,所以有一下简便方法使用DataBinding
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("recluse", "guo");
binding.setUser(user);
通过binding对象的各个getter和setter方法就可以控制各个变量。如本例中通过setUser()方法为其设置对象变量。
通过上述三个步骤就实现了控制逻辑代码对界面属性的控制,相比于之前的通过findViewById获取控件,然后使用View控件属性的setter方法设置控件属性的方式,唯一优势就是可以使用对象变量的多个属性域设置多个控件属性,但这只是我所理解的数据绑定部分的优势(可能还会有其他优势吧),data binding的优势并不只是如此,后面还有事件绑定以及双向的数据绑定部分,更能凸显data binding的优势。
事件绑定
由于XML是一种标记语言,其逻辑控制能力很弱,所以Android系统中界面的事件处理通常是在Java代码中获取控件,并为控件绑定监听器的方式处理控件的点击等事件。同时,也可以为一些有设置了默认的clickable的控件设置onclick属性,为控件指定点击事件的响应方法,并在对应的activity或fragment中实现该方法。同样data binding也可是实现该功能,并且更进一步。
在data binding中对事件的绑定有两种方式,第一种叫Method References,这个与原来的设置onclick属性并没有太大差别,如下为实例:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers"/>
<variable name="user" type="com.recluse.User"/>
</data>
<LinearLayout
....
<Button
android:text="click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{handler::onClickFriend}"/>
</LinearLayout>
</layout>
public class MyHandler {
public void onClickFriend(View view){
Log.d("recluse", "on click friend");
}
}
//MainActivity.java中设置handler对象变量
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("recluse", "guo");
MyHandler handler = new MyHandler();
binding.setUser(user);
binding.setHandler(handler);
在上述实例中设置了一个handler的对象变量,并在onClick属性中设置处理点击事件的方法,语法类似于C++中对方法的引用,使用双冒号。这种方式与之前的在activity中实现点击方法并无不同,这里定义的方法还是要与OnClickListener#onClick()方法的定义形式一致,即参数列表相同,否则报错,这里只不过可以将方法定义在任意的类中,并在XML文件中以对象变量的形式引入,并使用对象的方法而已。如果我们想在点击事件的处理中传入其他参数,比如我们想点击控件是改变User对象,将user变量传入,这时这种方式就无能为力了,因为不能改变参数列表。于是可以实现下面这种事件绑定的第二种方式。在官方文档中叫Listener Bindings。
首先看一下实例改为如下形式:
<?xml version="1.0" encoding="utf-8"?>
<layout>
...
<Button
android:text="click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{(view)->handler.onClickFriend(view, user)}"/>
</LinearLayout>
</layout>
//MyHandler.java
public void onClickFriend(View view, User user){
user.setFirstName("qian");
Log.d("recluse", "view id is " + view.getId());
Log.d("recluse", "on click friend and the user's last name is " + user.getLastName());
}
如上述实例中,在onClick属性中使用lambda表达式,对于参数列表,即->之前的那个括号,只可以使用两种形式,一是空,二是与属性所对应的定义方法参数列表相同,本例中为onClick,则需要一个view, 后面的方法中使用该view,这里代表这个button(可以通过打印出的id验证),如果后面的方法调用中不使用这个view,则可以使用第一个形式,即空参数列表。这里虽然限制lambda表达式的参数列表形式,但是后面的函数调用则可以任意的自定义函数参数列表了,如本例中传入了user变量对象,通过输出打印的last name可以验证我们传入的user对象就是原来在activity中定义的user,但是这里存在一个问题,我们设置了first name,但是界面并没有随之改变,除非我们再次调用ActivityMainBinding.setUser(user)。如果我们想要在设置的对象变量,如本例中的user, 发生变化时界面也可以随之改变,就可以使用data binding提供的双向绑定功能。
双向绑定
data binding的双向绑定是通过让POJO(plain old java object)类实现Observable接口,该接口中定义了添加和移除PropertyChangedListener接口的方法,通过监听器的方式监听对象状态的变化并通知界面View改变对应属性。由于过程较为复杂,data binding为我们提供了BaseObservable类,POJO类只需要继承BaseObservable,并在属性对应的getter方法上添加@Bindable注解以及setter方法中添加notifyPropertyChanged(BR.propertyName)方法即可,实例如下:
public class User extends BaseObservable{
private String firstName;
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Bindable
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
如代码中所示,当我们给属性对应的getter方法添加@Bindable注解之后,就会在BR类中生成一个该属性对应的入口,如BR.firstName,将该参数传入notifyPropertyChanged()方法,就可以在该属性发生变化时及时更新界面View的控件属性。此时我们点击按钮就可以改变firstName对应的TextView了。
此外,如果我们的POJO类由于框架限制不能继承BaseObservable, 而实现Observable接口并实现所有的监听和通知逻辑又十分繁琐和复杂。考虑到这种情况,data binding还为我们提供了更为方便的ObservableField类,以及Java中基本数据类型所对应的便捷类,如ObservableBoolean, ObservableInt等一系列类。
因此User类可以改成如下方式:
public class User{
public ObservableInt age;
public ObservableField<String> firstName;
public ObservableField<String> lastName;
public User(int age, String firstName, String lastName){
this.age = new ObservableInt(age);
this.firstName = new ObservableField<>(firstName);
this.lastName = new ObservableField<>(lastName);
}
}
XML的代码如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.recluseguo.recluseandroid.data.User"/>
<variable
name="handler"
type="com.recluseguo.recluseandroid.data.MyHandler"/>
</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}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(user.age)}"/>
<Button
android:id="@+id/click_bt"
android:text="click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{(view)->handler.onClickFriend(view, user)}"/>
</LinearLayout>
</layout>
MyHandler.java的代码如下:
public class MyHandler {
public void onClickFriend(View view, User user){
user.firstName.set("qian");
Log.d("recluse", "view id is " + view.getId());
Log.d("recluse", "on click friend and the user's last name is " + user.lastName.get());
}
}
从代码中可以看出我们使用ObservableField以及对应的便捷类等,对于对象状态的监听以及通知界面类操作不需要我们去考虑,很方便地实现了双向保定,此时每个属性都变成了Observable的一个子类,我们可以通过其set(T)和get()方法设置或获取属性值,如MyHandler中处理的那样,XML文件生成的对应ViewDataBinding类中获取对象属性也是使用相同方法,因此我们需要将对象变量的属性设置为public,如本例中firstName是public ObservableField, XML文件中设置的@{user.firstName},对应ActivityMainBinding类中设置该属性时会调用user.firstName.get()方法获取文本数据。
此外,对年龄的设置中可以看出在@{}大括号内部我们可以使用简单的Java表达式对变量做处理,这样在XML这种标记语言的环境下也可以执行一定的逻辑控制操作了,这样有些类似于JSP页面中的脚本。后面介绍data binding的一些语法细节中也可以看出其他与JSP相似的地方。
这里我们是使用一个POJO类封装关于一个类的多个属性,data binding同时还为我们提供了更为方面的组织一系列属性的方式,就是Observable的第三种形式,Observable Collection。Collection包括ObservableArrayList和ObservableArrayMap
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<data>
<import type="android.databinding.ObservableList"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text='@{user[0]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[2])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
使用ObservableArrayMap
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
对应的XML文件
<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"/>
这里的例子很容易看懂,不过有个新的知识点是标签的子标签,它负责引入在XML文件中使用的类,后面语法细节部分会讲述。这里需要注意的是ObservableMap是一个接口,它继承自Map,并且在内部定义了与Observable相同的接口方法, ObservableArrayMap是它的一个实现类,所以在代码中直接使用ObservableArrayMap即可以实现一些简单的一系列双向绑定的属性的组织。同样的,ObservableArrayList也是实现了ObservableList接口的类,用于实现简单的一系列双向绑定属性的组织。其内部具体的实现源码暂时不做分析。
所以总结双向数据绑定有三种实现方式,一是继承BaseObservable,并对属性getter方法使用@Bind注解,而且不要忘记在setter方法中调用notifyProperChanged()方法,传入参数是BR类中生成的该属性的入口;第二种方式就是对象中直接定义ObservableField属性,需要定义为public,不再需要getter和setter,通过ObservableField的set()和get()方法完成属性的设置和获取,泛型参数T代表属性的类型,对应基本数据类型有一系列的便捷类与之对应。第三种就是最方便的方式,就是使用Observable Collection的形式组织一系列的属性。
至此就完成data binding的基本内容的讲述,包括数据绑定,事件绑定以及事件的双向绑定。下面还有一些具体的语法细节和面临的一些其他问题,下面一部分展开讲述。
语法细节
1. import标签
前面说过在@{}中可以使用一些简单的Java表达式,包括Lambda表达式,如果在这些表达式中使用了某些Java类,并不属于java.lang包,我们都知道在java代码中需要通过import导入包,在data binding中也在XML文件中引入了相同的语法,即标签中的子标签,前面已经用过该标签,type属性即表示引入的Java类,为了避免引入时带来名称冲突,还可以使用alias属性为该类指定别名,如下:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
这一点感觉也有点像JSP页面。
2. View with ID
使用data binding,我们可以通过向XML文件中传入变量的形式控制控件的属性,因此大部分情况下,我们不需要再Java代码中获取到某个控件的引用,因此添加ID也就没有太大必要,但是少数情况需要在Java代码中获取某个控件时,我们还是可以为该控件设置ID,但是此时我们不需要findViewById去获取控件引用,而是在XML文件生成的对应ViewDataBinding类中生成一个域与该控件对应,在Java代码中直接使用该控件即可。如下:
<TextView
android:id="@+id/first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
在XML文件中为TextView设置了Id, 在ActivityMainBinding类中会生成一个域:public final TextView firstName; 则在Java代码中直接使用binding(ActivityMainBinding的实例化对象),binding.firstName可以获取该TextView的引用。
3. 自定义类名
<data class=".ContactItem">
...
</data>
也可以指定包,即指定其全限定名
<data class="com.example.ContactItem">
...
</data>
4. includes
在一个XML文件中,包含另外的XML文件中,也可以将某个变量传递到被包含文件中的变量对象,但是包含文件以及被包含文件都需要定义了该变量。
使用方式如下:
<?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>
name.xml和contact.xml文件中也必须定义了user变量,它们两个中的user变量则由该包含XML文件传递,即ActivityMainBinding#set(user), 那么在name.xml和contact.xml文件中也可以使用该user对象了。
5. @{}内的语法
在大括号内通常是引用变量的某个属性或方法,也可以是一个Lamada表达式。总结来说可以是Java的一个表达式,但是不支持new,super,this等关键字,这里只是表达式。大括号内还可以引用资源文件,如@string/str_name等可以在xml文件中使用的引用资源文件的方式都可以使用。
一些问题
我们知道data binding只是帮我们完成一件事件,就是监听对象状态的变化,并且通知对应的界面控件属性发生改变,这一系列动作都是由ViewDataBinding类帮助我们完成的,而它完成这些功能的方式就有一个就是调用对应属性的getter和setter方法。我们自定义的类或者Observable类,即用于XML对象变量的类,所有属性要么是public的要么有其对应的getter方法,即使是ObservableField类的属性,也可以设置为public,并访问该属性的get()方法获取真正想要赋值到View控件上的属性的值,这一步容易理解,而下一步就是设置控件的属性,也很简单就是调用控件View对应属性的setter方法。但是这里就存在一个问题,某些控件的属性设置方法,其方法名不是setXXXX(),这里就会遇到问题,我们的ViewDataBinding类如何设置这些属性。官方文档上给出了两种解决办法:
1. 重命名setter
官方文档给出的实例是ImageView, 它的android:tint属性设置实际上应该调用ImageView#setImageTintList(ColorStateList)方法,而不是setTint(),所以设置重命名的setter,方法如下:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
这个注解是添加到ImageView当中,而我们不能修改ImageView的代码,所以这种方式对开发者来说作用不大,唯一可以用的地方是在自定义View的时候可以添加重命名的注解,但是自定义View的时候编写正确方式setter就可以了,也没必要使用。Android系统提供的控件,其属性的setter方法基本上都是使用该注解重命名过的,所以可以放心使用data binding。而自定义的View,我们自己定义setter即可。
2. 使用BindingAdapter
由于我们对于系统提供的控件无法改变,而有些属性是没有对应setter方法,如官方文档的举例中说,paddingLeft属性对应着setPadding()方法,该方法是设置四个方向的padding, 并没有单个方法的设置方法,此时我们使用data binding想要设置这类型的属性,我们就需要自己定义方法做处理,同时又不需要修改系统控件的代码,data binding为我们提供了这种方式,即@BindingAdapter注解。如下实例:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
该方法是定义在任意一个类中的静态方法,比如我们可以定义一个BindingAdapterUtil.java类,在其中定义该静态方法,只要有@BindingAdpter()注解以及其中参数参数表示的XML中的属性,data binding就可以找到该静态方法并调用,该方法的参数列表是该控件view和注解参数中所表示的控件属性值共同组成。@BindingAdapter()注解可以包含多个属性,即参数可以是一个String数组,其中包括若干属性,对应静态方法则需要一个view参数,后面跟着String数组表示属性的对应属性值,共同组成方法的参数列表。这些属性值是由data binding传入的,至于值是什么,则是由XML文件中该属性使用的对象变量的绑定决定的。
通过这种方式,我们就可以自己定义方法,供data binding在给控件的属性设置属性值调用setter方法时调用,从而避免了调用控件View的属性对应setter方法。
既然我们可以为data binding指定在设置属性时的方法,那么我们就可以为控件设置任意的属性,即使该控件没有的属性也是可以的,只不过是在XML文件中我们使用自定义的命名空间,并在自定义的@BindingAdapter方法中对这些属性做处理,完成对控件的设置即可。比如官方的实例,我们想要通过Picasso加载图片,就可以使用如下方式:
@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}"/>
这里在@BindingAdapter注解中,使用了bind的命名空间,其实bind关键字是指对于XML中的属性的处理忽略命名空间,只要是imageUrl和error属性,其值是由data binding绑定的,都经过该静态方法做处理,包括自定义的命名空间以及android命名空间。这里由于ImageView并没有imageUrl和error属性,所以需要使用自定义的命名空间,如果使用android:imageUrl 是会报错的。
接下来时类型匹配问题,之前说@{}可以是一个Java表达式,那么表达式则会返回一个Java对象,data binding会调用对应属性的setter方法,并将该Java对象作为参数传入,这里就存在一个类型匹配问题,系统首先会将java对象自动转型为参数类型,如果转型失败则会发生错误。考虑之前说的@BindingAdapter,我们可以想到一个解决方法,就是自定义静态方法,使得data binding不再调用setter方法,只要我们自定义的静态方法可以接受这个java对象的类型,并在方法中做转换处理并设置控件正确的属性即可。(官方文档上并没有如此说明,只是想到可以这样实现,可能会有一些其他问题,比如由于是设置的android命名空间的属性,有可能其他控件的该属性使用data binding时也会调用该静态方法,造成不必要的麻烦,具体还没有深入分析)
对于类型匹配问题,data binding 为我们提供了另外一个注解,@BindingConversion, 如下实例:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
由于background需要的类型是drawable, 而data binding的表达式返回的明显是int类型,因此需要我们自己定义一个conversion,这个静态方法也是可以定义在任意的类中,只需要@BindingConversion注解即可
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
这样在data binding在调用setter方法时,会调用该转换方法。
除此以外还有事件绑定的部分,每个监听器只能有一个方法,否则需要拆成两个等问题,暂时没有用到还没有去细致的学习,另外还有一些问题,比如在定义了@BindingAdapter({“bind:property”, XXXX})的静态方法后,会拦截所有的名称为property的setter方法调用,而改为调用此方法,控件较多时会不会发生混乱,定义多个@BindingConversion时,data binding是如何查找正确的方法的,仅仅是根据参数类型和返回值类型做出判断吗,如果定义两个方法不同名,但是参数类型和返回值类型完全一样,但是需要不同逻辑,会不会发生混乱。等以后再做研究,基本用法暂且介绍到这里。