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 , 我们可以通过声明式布局以精简的代码来绑定应用程序逻辑和布局,这样就不用编写大量的毫无营养的代码了。

构建环境

  1. 首先,确保能使用Data Binding,需要下载最新的 Support repository。否则可能报错,如图:

  2. 在模块的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 节点。举个例子, 以下的代码<font color = "red">不能正常运行 </font>:

<?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
ObservableByteObservableCharObservableShortObservableIntObservableLongObservableFloatObservableDouble
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:layout="@layout/include"
            ... />
    </LinearLayout>
</layout>

在 Java 代码中获取 binding 实例,为 ViewStubProy 注册 ViewStub.OnInflateListener 事件:

mActivityViewStubBinding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
mActivityViewStubBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
    @Override
    public void onInflate(ViewStub stub, View inflated) {
        IncludeBinding viewStubBinding = DataBindingUtil.bind(inflated);
        User user = new User("Connor", "Lin", 28);
        viewStubBinding.setUser(user);
    }
});

通过 ViewStubProxy 来 inflate ViewStub :

public void inflate(View view) {
    if (!mActivityViewStubBinding.viewStub.isInflated()) {
        mActivityViewStubBinding.viewStub.getViewStub().inflate();
    }
}

此处 isInflated()getViewStub() 会标红,请不要担心,这并不是错误,是 ViewStubProxy 中的方法。

高级绑定

动态变量

有时候,有一些不可知的 binding 类。例如,RecyclerView.Adapter 可以用来处理不同布局,这样的话它就不知道应该使用哪一个 binding 类。而在 onBindViewHolder(VH, int) ) 的时候,binding 类必须被赋值。

在这种情况下,RecyclerView 的布局内置了一个 item 变量。 BindingHolder 有一个 getBinding 方法,返回一个 ViewDataBinding 基类。

public void onBindViewHolder(BindingHolder holder, int position) {
  holder.getBinding().setVariable(BR.item, mItemList.get(position));
  holder.getBinding().executePendingBindings();
}

以上,详细请参考 DataBindingDemo -> MainActivity 部分(使用 RecyclerView 实现)。

直接 binding

当变量或者 observable 发生变动时,会在下一帧触发 binding。有时候 binding 需要马上执行,这时候可以使用 executePendingBindings()

后台线程

只要数据不是容器类,你可以直接在后台线程做数据变动。Data binding 会将变量/字段转为局部量,避免同步问题。


属性设置

本部分源码请参考 DataBindingDemo -> AttributeSettersActivity 部分。

当绑定数据发生变动时,生成的 binding 类必须根据 binding 表达式调用 View 的 setter 函数。Data binding 框架内置了几种自定义赋值的方法。

自动设置属性

对一个 attribute 来说,Data Binding 会尝试寻找对应的 setAttribute 函数。属性的命名空间不会对这个过程产生影响,只有属性的命名才是决定因素。

举个例子,针对一个与 TextView 的 android:text 绑定的表达式,Data Binding会自动寻找 setText(String) 函数。如果表达式返回值为 int 类型, Data Binding则会寻找 setText(int) 函数。所以需要小心处理函数的返回值类型,必要的时候使用强制类型转换。

需要注意的是,Data Binding 在对应名称的属性不存在的时候也能继续工作。你可以轻而易举地使用 Data Binding 为任何 setter “创建” 属性。
DataBindingDemo 中的自定义布局 Card,并没有添加 declare-styleable,但是可以使用自动 setter 的特性来调用这些函数。

<com.connorlin.databinding.view.Card
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:object="@{user}"/>

重命名属性设置

一些属性的命名与 setter 不对应。针对这些函数,可以用 BindingMethods 注解来将属性与 setter 绑定在一起。举个例子, android:tint 属性可以这样与 setImageTintList(ColorStateList) ) 绑定,而不是 setTint :

@BindingMethods({
      @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

Android 框架中的 setter 重命名已经在库中实现了,我们只需要专注于自己的 setter。

自定义属性设置

一些属性需要自定义 setter 逻辑。例如,目前没有与 android:paddingLeft 对应的 setter,只有一个 setPadding(left, top, right, bottom) 函数。结合静态 binding adapter 函数与 BindingAdapter 注解可以让开发者自定义属性 setter。

Android 属性已经内置一些 BindingAdapter。例如,这是一个 paddingLeft 的自定义 setter:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

Binding adapter 在其他自定义类型上也很好用。举个例子,一个 loader 可以在非主线程加载图片。

当存在冲突时,开发者创建的 binding adapter 会覆盖 Data Binding 的默认 adapter。

你也可以创建多个参数的 adapter:

// 无需手动调用此函数
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
    Glide.with(view.getContext()).load(url).error(error).into(view);
}
<!-- 当url存在时,会自动调用注解方法,即loadImage()-->
<ImageView 
    app:imageUrl=“@{url}”
    app:error=“@{@drawable/ic_launcher}”/>

当 imageUrl 与 error 存在时这个 adapter 会被调用。imageUrl 是一个 String,error 是一个 Drawable。

  • 在匹配时自定义命名空间会被忽略
  • 你可以为 android 命名空间编写 adapter

Binding adapter 方法可以获取旧的赋值。只需要将旧值放置在前,新值放置在后:

@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());
  }
}

事件 handlers 仅可用于只拥有一个抽象方法的接口或者抽象类。例如:

@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);
        }
    }
}

当 listener 内置多个函数时,必须分割成多个 listener。例如, 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);
}

因为改变一个 listener 会影响到另外一个,我们必须编写三个不同的 adapter,包括修改一个属性的和修改两个属性的。

@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 是 add/remove View.OnAttachStateChangeListener 而不是 setandroid.databinding.adapters.ListenerUtil可以用来辅助跟踪旧的 listener 并移除它。

对应 addOnAttachStateChangeListener(View.OnAttachStateChangeListener) )支持的 api 版本,
通过向 OnViewDetachedFromWindowOnViewAttachedToWindow 添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1) 注解,
Data Binding 代码生成器会知道这些 listener 只会在 Honeycomb MR1 或更新的设备上使用。


转换器Converters

对象转换

当 binding 表达式返回对象时,会选择一个 setter(自动 Setter,重命名 Setter,自定义 Setter),将返回对象强制转换成 setter 需要的类型。

下面是一个使用 ObservableMap 保存数据的例子:

<TextView
  android:text='@{userMap["lastName"]}'
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>

在这里, userMap 会返回 Object 类型的值,而返回值会被自动转换成 setText(CharSequence) 需要的类型。当对参数类型存在疑惑时,开发者需要手动做类型转换。

自定义转换

有时候会自动在特定类型直接做类型转换。例如,当设置背景的时候:

<View
  android:background="@{isError.get() ? @color/colorAccent : @color/colorPrimary}"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>

在这里,背景需要的是 Drawable ,但是 color 是一个整数。当需要 Drawable 却返回了一个整数时, int 会自动转换成 ColorDrawable 。这个转换是在一个 BindingConversation 注解的静态函数中实现:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) { 
    return new ColorDrawable(color); 
}

需要注意的是,这个转换只能在 setter 阶段生效,所以 不允许 混合类型:

<View
  android:background="@{isError.get() ? @drawable/error : @color/colorPrimary}"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>

Android Studio对Data Binding的支持

  • Android Studio 支持 Data Binding 表现为:

    • 语法高亮
    • 标记表达式语法错误
    • XML 代码补全
    • 跳转到声明或快速文档

注意:数组和泛型类型,如 Observable 类,当没有错误时可能会显示错误。

  • 在预览窗口可显示 Data Binding 表达式的默认值。例如:
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName, default=FirstName}"/>
  <!-- TextView 的 text 默认值为 FirstName -->

如果你需要在设计阶段显示默认值,你可以使用 tools 属性代替默认值表达式,详见 设计阶段布局属性

参考资料

  1. Data Binding Library
  2. 安卓 Data Binding 使用方法总结
  3. (译)Data Binding 指南
  4. 精通 Android Data Binding

我的简书账号是 ConnorLin,欢迎关注!

我的简书专题是 Android开发技术分享,欢迎关注!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容