Data Binding 详解(一)-从零开始

知是行之始,行是知之成。
文章配套的 Demohttps://github.com/muyi-yang/DataBindingDemo
Demo 支持 Java 和 Kotlin 双语言,master 分支为 Java 语言代码,kotlin 分支为 Kotlin 语言代码。

DataBinding介绍

2015 年的 Google IO 大会上,Android 团队发布了一个数据绑定框架(Data Binding Library),它是为了解决数据和 UI 的绑定问题,同时也是对 MVVM 模型的一个实践和引领。MVVM 模型不了解的请自行补上。

优点

  • 在 XML中绑定数据,XML变成UI的唯一真实来源
  • 去掉 Activities & Fragments 内的大部分 UI 代码(setOnClickListener, setText, findViewById)
  • 数据变化可自动刷新 UI,同时保证 UI 操作都在主线程运行

缺点

  • IDE 支持还不完善,在 XML 中代码提示、表达式验证都有很大缺失
  • 有些报错信息不明显,初学者排错难度较大
  • 重构支持较差,数据绑定写在 XML 中,丧失面向对象特性

开启 DataBinding

Gradle 配置

想在你的应用程序中使用 Data Binding,需要在应用程序 module 中的 build.gradle 文件中添加 dataBinding 配置,此配置将会在你的项目里添加必要的 Data Binding 插件以及编译配置依赖。

android {
    ....
    dataBinding {
        enabled = true    
    }    
}

注意:你必须为依赖于使用 Data Binding 的库(aar)的应用程序在 gradle 中增加开启 Data Binding 的配置,即使这个 module 没有直接使用 Data Binding 也需要。
例如: A module 依赖 B module,B module 又依赖 C module,但只有 C module 中使用了 Data Binding ,这个时候 A B C 三个 module 都必须在 gradle 中增加以上配置,否则会在 Data Binding V1 编译器中编译不过,在 V2 编译器中可编译,但会运行时出错。

Android Gradle 插件在版本 3.1.0-alpha06 包含一个新的 Data Binding 编译器(V2),用于生成 Binding 类。它的主要改变有:

  1. 新的编译器是增量编译 Binding 类,这在大多数情况下加快了编译速度。
  2. library 模块的 Binding 类会被编译并打包到 AAR 文件中。 依赖这些库的应用程序不再需要重新生成 Binding 类。
  3. 老版本在编译出错时,经常会出现一些与真真错误不符合的提示,这个问题在新版本中已经做了修改。
  4. 绑定适配器(binding adapters)只影响自己 module 中的代码和 module 的使用者,它不能更改 module 依赖库中的适配器的行为。( 绑定适配器后面我们会详细讲解)

要启用新的 Data Binding 编译器,请在 gradle.properties 文件中添加以下选项:

android.databinding.enableV2=true

或者在 gradle 命令中通过添加以下参数来启用新的编译器:

-Pandroid.databinding.enableV2=true

但是这个编译器在 Android Studio 3.2 版本中已经是默认启用的状态,所以你如果是 Android Studio 3.2 版本及以上版本可以不用关心这个特征。

注明:Data Binding 提供了兼容,它可以支持 Android 4.0(API 14)及以上系统。Data Binding 插件需要 Android Studio 1.3.0 及以上版本,Gradle 1.5.0 及以上版本才能正常工作。
本文章及例子是基于 Android Studio 3.3.1版本,Gradle 4.10.1 版本。电脑系统是 Ubuntu 16.04(有些问题跟系统有关)。

XML写法

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <android.support.constraint.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        <TextView
            android:id="@+id/tv_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </android.support.constraint.ConstraintLayout>
</layout>

Data Binding 的布局文件有点不同的是:起始根标签是 layout,标签里面的内容和普通的布局没有区别。

Data Binding 插件会检索所有布局文件,会把根标签为 layout 的布局编译出一个继承自 ViewDataBinding 的类(build 目录下)。其命名规则是根据布局文件名来的,比如 activity_main.xml,那么生成的类就是 ActivityMainBinding。ActivityMainBinding 类是一个抽象类,它的实现类是 ActivityMainBindingImpl(也在build目录下),它的作用就是实现了 Data Binding 的一系列功能和特征。什么功能和特征?别急,后面会讲到!我们先看如何使用 Data Binding 布局。

布局使用

前面说了 Data Binding 的功能和特征在自动生成的 ActivityMainBinding 类中,那我们需要获取到 ActivityMainBinding 对象才能使用它。使用了 Data Binding 的布局在 Activity 中就不能直接调用 setContentView(R.layout.activity_main)设置布局了,你得这样做:

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
}

在 Activity 中通过 DataBindingUtil 工具类的 setContentView 方法设置布局到 Activity 当中,同时返回
ActivityMainBinding 对象。有了 ActivityMainBinding 对象,我们就可以去体验 Data Binding 的魅力了。

如果你是在代码运行时创建View,想使用 Data Binding,你可以通过如下方式获取绑定类:

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());

如果你是在 Fragment、ListView 或者 RecyclerView 的适配器中使用 Data Binding,你可以使用
DataBindingUtil 类的 flatflate() 方法,如下面的代码示例所示:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//或者
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

使用绑定类中的 View

使用了 Data Binding 我们不在需要 findViewById() 获取对象了,因为 Data Binding 编译插件会检索布局文件中的控件,把已经声明了 ID 的 View 自动创建对象到 ActivityMainBinding 类中,直接获取使用即可。对象的命名规则是根据控件 ID 名来的,比如 android:id="@+id/tv_info",自动生成后的对象名为 tvInfo(去除下划线,并以驼峰格式命名):

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    binding.tvInfo.setText("我是使用Data Binding的Demo");
}

以上是通过 ActivityMainBinding 对象获取布局中 TextView 的对象 tvInfo 并设置新值。

数据绑定

要进行数据绑定,首先要在布局中声明绑定变量,声明变量需要使用 data 标签以及 variable 标签。data 标签里面是用来做一些声明,比如声明变量,导入数据类型等。

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="com.example.databindingdemo.bean.UserInfo" />
        <variable
            name="user"
            type="UserInfo" />
    </data>
    .....
</layout>

这是一个用户信息界面的数据绑定,import 标签是导入一个数据类型,variable 标签是声明一个类型为 UserInfo 的变量 user

绑定属性

数据实体类:

public class UserInfo {
    public String name;
    public int age;
    public int sex;
    public String sign;
}

完整布局:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="com.example.databindingdemo.bean.UserInfo" />
        <variable
            name="user"
            type="UserInfo" />
    </data>
    <android.support.constraint.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="@{@string/name(user.name), default=姓名}"
            android:textSize="18sp"
            app:layout_constraintLeft_toLeftOf="parent" />
        
        <TextView
            android:id="@+id/tv_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="@{@string/age(String.valueOf(user.age)), default=年龄}"
            android:textSize="18sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_name" />

        <TextView
            android:id="@+id/tv_sex"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="@{@string/sex(user.sex == 1?@string/sex_man:@string/sex_woman), default=性别}"
            android:textSize="18sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_age" />

        <TextView
            android:id="@+id/tv_sign"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="@{user.sign, default=个性签名}"
            android:textSize="18sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_sex" />
    </android.support.constraint.ConstraintLayout>
</layout>

在 layout 中的表达式使用“@{}”语法在属性中编写,完整布局中有多个这样的表达式,比如为 TextView 设置值:

<TextView
     android:id="@+id/tv_sign"
    ...
     android:text="@{user.sign, default=个性签名}"
    ...
 />

这里的“@{}”中的表达式是从 user 对象(前面声明的变量)中获取 sign 字段绑定到了 TextView 中,后面的 default 属性是用来设置布局预览时的值,它是可选字段,也可以直接写成 android:text="@{user.sign}",只不过这样写在布局预览时就不会显示内容。

以下为预览效果:
image.png

小插曲:本 Demo 一开始是基于 Ubuntu 系统写的,在 xml 中写中文是没问题的,但后来在 Win 系统上运行 Demo 则报 Caused by: org.apache.xerces.impl.io.MalformedByteSequenceException: Invalid byte 2 of 3-byte UTF-8 sequence. 错误,这是因为 Win 在编译中文时的编码问题(Demo 已修正)。在此提醒广大读者,不要偷懒,应该把所有中文都写到 string.xml 中去,在 xml 中这样引用android:text="@{@string/name(user.name), default=@string/default_name}",特别是在使用了 Data Binding 的情况下,这样会减少很多迷之编译错误。

绑定方法

前面讲了绑定对象的属性,还可以绑定对象的方法,比如:

public class UserInfo {
    private String name;
    ...
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    ...
}

有很多时候我们的对象属性是私有的,我们提供了 Getter 和 Setter 方法,在绑定的时候我们可以这样写android:text="@{user.name}", 为什么不是android:text="@{user.getName()}"呢?这是因为 Data Binding 内部做了处理,它会把 getName() 方法解析为 name(),所以我们可以直接使用表达式 @{user.name}

注意:只要项目中开启了Data Binding功能,所有的 getxxx 方法都遵循这个规则。

对于类中的字段、 getter 方法和 ObservableField 对象都可以在表达式中使用格式引用:

android:text="@{user.name}"

在编译生成的绑定类中也会自动检查空值并避免空指针异常。 例如,在表达式 @{ user.name } 中,如果 usernull,则 user.name 的默认值为 null。 如果表达式 @{ user.age },其中 age 类型为 int,那么数据绑定使用默认值 0,其他数据类型类似。

设置数据

布局写好后,我们只需在代码中绑定对应数据即可:

    private ActivityUserBinding binding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_user);

        UserInfo info = new UserInfo();
        info.name = "木易";
        info.age = 28;
        info.sex = 1;
        info.sign = "问君能有几多愁,恰似一杯二锅头";
        binding.setUser(info);
    }

以上是获取 ActivityUserBinding 对象,调用 setUser 方法绑定数据,setUser 方法是自动编译生成的,命名规则是根据布局中声明的变量 user 字段(<variable name="user" type="UserInfo" />)而定的,在布局中声明的所有变量都会自动生成 Getter 和 Setter 方法。当调用 setUser 方法时,会自动触发所有绑定了 UserInfo 对象的 View 重新赋值,当界面刷新时 UserInfo 中的信息就显示在界面上了。

至此 Data Binding 算是用上了,但仅仅只是打了个照面,好比能运行 “hello word” 了。这只是一个开始,接下来的几篇文章会比较详细的讲解 Data Binding 的功能和特性。

此篇到这里就结束了,可以查看下一篇 Data Binding 详解(二)-布局和绑定表达式

如果你觉得文章有帮助到你,记得点个喜欢以表支持,同时欢迎你的指正和建议。十分感谢!