要讲到mvvm模式的开发,就不得不提DataBinding。两者结合才是一个完整体。关于使用,直接官网:Data
Binding Guide

DataBinding 库是 Google 公司 Android Framework UI
工具团队开发出来的一款 Android 库。DataBinding 库增强了 Android
项目中的布局文件对 UI 的控制能力,以前对 UI 进行控制和响应 UI
发出的命令的代码逻辑,现在就可以直接放在布局文件中使用 DataBinding
表达式
来表达,和 Java
代码中的数据直接关联。通过这种声明式编程,可以大大减少 UI 和
逻辑代码之间的“胶水代码”,比如以前 Activity/Fragment 中的 UI
控制逻辑;从而增加了 layout xml 文件的表现力,当然也增加了 layout xml
文件的复杂程度,控制好 layout xml 文件中 bingding
表达式的复杂程度,把所有业务逻辑仍然放在 java 代码中,是使用好
DataBinding 库的关键。负责动画控制的逻辑建议仍然放在 java 代码中。
使用DataBinding库以后你至少可以得到以下好处:

  1. 不用写 setOnClickListener 之类的响应 UI 命令的代码(响应 view
    命令)
  2. 不用写 setText() 之类的控制 view 属性的代码(控制 view)
  3. 不用写 findviewbyid 代码(2 的附加产物)

简单一句话,DataBinding 让你可以在布局文件中写 java
表达式,所以可以省略掉中间层。可以说 DataBinding 库是减少甚至完全代替
view 和 业务逻辑 之间中间层 stupid code 的利器。

Android DataBinding

Data Binding
Library
从 2015 Google I/O 上发布到至今,已经有一年多的长足发展,目前在 Android
Studio2.2 版本上已经拥有比较成熟的使用体验。可以说 Data Binding
已经是一个可用度较高,也能带来实际生产力提升的技术了。

对应的中文翻译:Data Binding 用户指南

目录

编译环境

2.0 版本以后的 Android Studio 已经内置支持了 DataBinding ,我们只要在
gradle 文件中添加如下代码就可以使用 Databinding:

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

xml 文件的处理

<layout>
    <data class = "CustomBinding">
    </data>
    // 原来的layout
</layout>

layout标签位于布局文件最外层,可以使原来的普通布局文件转化为
databinding layout
,同时会在build/ganerated/source/apt下相关目录下生成 ***Binding 类

默认生成规则:xml通过文件名生成,使用下划线分割大小写,即
activity_main.xml 会生成对应的 ActivityMainBinding

data标签用于申明 xml 文件中的变量用于绑定
View,可以通过对标签的修饰来指定生成 Binding
类的自定义名称,如上述的布局文件最终会生成一个 CustomBinding 类

Java 代码的处理
需要用 DataBindingUtil 类中的相关方法取代原先的 setContentView 及
inflate 获得 ***Binding 实例类

一个github项目,精通DataBinding的所有用法:MasteringAndroidDataBinding

1. 搭建构建环境
2. Data Binding 中的布局文件入门
  2.1. 编写 data binding 表达式
  2.2. 数据对象
  2.3. 绑定数据
  2.4. 事件处理
    2.4.1. 方法引用绑定
    2.4.2. Lisenter 绑定
    2.4.3. 避免复杂侦听器
3. Data Binding 中的布局文件详解
  3.1. import
  3.2. 变量
  3.3. 自定义 Binding 类名
  3.4. Includes
  3.5. 表达式语言
4. 数据对象
  4.1. Observable 对象
  4.2. Observable 属性
  4.3. Observable 容器类
  4.3. 双向绑定
5. 生成绑定
  5.1. 创建绑定
  5.2. 带有 ID 的 View
  5.3. 变量
  5.4. ViewStub
  5.5. 高级绑定
    5.5.1. 动态绑定变量
    5.5.2. 立即 binding
    5.5.3. 后台线程问题
6. 属性 Setter
  6.1. 自动 Setter
  6.2. 重命名 Setter
  6.3. 自定义 Setter
7. 转换器
  7.1. 对象转换
  7.2. 自定义转换
8. Android Studio 对 Data binding 的支持

取代findViewById方法

findViewById(int id) 方法是将 View 的实例与 xml 布局文件中的 View
对应赋值的过程,需要遍历所有的 childrenView
查找。更关键的一点是如果比较复杂的页面,可能会存在数十个控件,光写
findViewById 也会让人挺崩溃的。虽说有着诸如 ButterKnife
这样优秀的第三方库,但使用数据绑定方式无疑更简洁明

private TextView mFirstNameTv;
private TextView mLastNameTv;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(this, R.layout.activity_first);
    mFirstNameTv = (TextView) findViewById(R.id.tv_first_name);
    mLastNameTv = (TextView) findViewById(R.id.tv_last_name);
}

//********* 或者使用 *********

private ActivityFirstBinding mFirstBinding;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 在mBinding中有布局文件中带id的View变量
    mFirstBinding = DataBindingUtil.setContentView(this, R.layout.activity_first);
}

采用 DateBinding 后,所有的 View 会在 Binding 实例类生成对应的实例,而有
id 的 View 则会使用 public
进行修饰,而变量名的生成规则是通过下划线分割大小写,即
id = "@+id/main_view" 会生成对应的 mainView 的变量,我们可以直接通过
binding.mainView 获取,直接节省了在 activity
中声明一长串变量的步骤,也不需要再写 findViewById 方法或者加上 @BindView
的注解

<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tv_first_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_last_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>  
    </LinearLayout>
</layout>

在 activity_first.xml 布局文件中添加 databindind 的 layout
标签后会生成 ActivityFirstBinding 类

// views
private final android.widget.LinearLayout mboundView0;
public final android.widget.TextView tvFirstName;
public final android.widget.TextView tvLastName;

带 id 的 view 最终会生成 public final 修饰的字段,而不带 id 的 view
也会生成 private final 修饰的字段。而这些则是在 ActivityLoginBinding
的构造函数中赋值的,仅仅只需要遍历一遍整个的 view 树,而不是多个
findViewById 方法遍历多次

使用bingding针对对应viewid进行设置。

这篇文档介绍了如何使用 Data Binding Library
来编写声明式的布局,这样可以减少应用中逻辑代码和布局之间所需要的“胶水代码”。

为布局文件绑定Variable

需要注意的是,布局xml中的根布局必须为layout,否则编译器无法生成对应的binding.

Data Binding Library 提供了非常好的灵活性与兼容性 - 它是一个 support
library,可以在 Android 2.1(API level 7+)及其以上的平台使用。

数据绑定getter和setter

Variable 是 DataBinding
中的变量,可以在data标签中添加variable标签从而在 xml 中引入数据

<layout>
    <data>
        <variable
            name="user"
            type="com.sanousun.sh.databinding.bean.User"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_first_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}"/>

        <TextView
            android:id="@+id/tv_last_name"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="@{user.lastName}"/>

    </LinearLayout>

</layout>

variable 就是普通的 POJO 类,实现 getter 方法,并没有提供更新数据刷新 UI
的功能

private static class User {

    private String firstName;
    private 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;   
    }
}

如果希望数据变更后 UI 会即时刷新,就需要继承 Observable 类

private static class User extends BaseObservable {

   private String firstName;
   private String lastName;

   @Bindable
   public String getFirstName() {
       return this.firstName;
   }

   @Bindable
   public String getLastName() {
       return this.lastName;
   }

   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }

   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

BaseObservable 提供了 notifyChange 和 notifyPropertyChanged
两个方法来刷新 UI ,前者刷新所有的值,而后者则是刷新 BR
类中有标记的属性,而 BR 类中的标记生成需要用Bindable的标签修饰对应的
getter 方法
同时 databinding 提供了 Observable** 开头的一系列基础类可以避免继承
BaseObservable

private static class User {
    public final ObservableField<String> firstName =
        new ObservableField<>();
    public final ObservableField<String> lastName =
        new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

本质上 Observable** 也是通过继承 BaseObservable
实现的,调用set方法时会调用 BaseObservable 的 notifyChange 方法

user.firstName.set("first");
String lastName = user.lastName.get();

//********************************

public void set(T value) {
    if (value != mValue) {
        mValue = value;
        notifyChange();
    }
}
public class ViewWithIDsActivity extends BaseActivity { ActivityViewWithIdsBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_view_with_ids); } public void showMyName(View view) { binding.firstName.setText; }}

要使用 data binding,Android 的 Gradle 插件必须是 1.5.0-alpha1
或更高版本。

运算表达式

运算符
支持绝大部分的 Java
写法,允许变量数据访问、方法调用、参数传递、比较、通过索引访问数组,甚至还支持三目运算表达式

  • 算术 + - * / %
  • 字符串合并 +
  • 逻辑 && ||
  • 二元 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <=
  • instanceof
  • Grouping ()
  • 文字 – character, String, numeric, null
  • Cast
  • 方法调用
  • Field 访问
  • Array 访问 []
  • 三目运算符 ?:

尚且不支持 this,super,new 以及显式的泛型调用

空指针处理
无需判断对象是否为 null,DataBinding 会自动检查是否为
null,如果引用对象为 null,那么所有属性引用的都是 null
的,你无需判断也不会导致崩溃

空合并运算符 ??
引用的对象为 null,需要做额外的判断,DataBinding 提供了空合并运算

android:text="@{user.firstName ?? user.lastName}"
//会取第一个非空值作为结果,相当于
android:text="@{user.firstName != null ? user.firstName : user.lastName}"

集合数组的调用
对于数组,List,Map,SparseArray的访问,我们可以直接通过[]的数组下标来访问,值得注意的是数组越界的问题

资源文件的引用
值得一说的是可以直接组合字符串

android:text="@{@string/nameFormat(firstName, lastName)}"

<string name="nameFormat">%s, %s</string>

也可以对数值类应用直接进行运算

android:marginLeft="@{@dimen/margin + @dimen/avatar_size}"

需要注意的是一些资源文件需要确切的名称

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

属性关联
DataBinding 库通过解析 View 的 setter
方法来完成赋值过程,android:text = "@user.firstName"就相关于调用了
TextView 的 tv.setText(user.firstName)

甚至可以调用 View 未提供的布局属性,只要 View 提供了对应的 setter
方法。
举个例子:

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"/>

DrawerLayout 有个
setScrimColor(int color)方法,所以可以在布局中使用未定义的app:scrimColor属性,通过
app 命名空间修饰的属性会自动关联到对应的方法

可以看到我们可以直接使用binding.firstName来对指定的view进行操作,再也不需要使用findViewbyId或者其他注解的方式了。

1. 搭建构建环境

  1. 在 Android SDK Manager 中下载最新的 Support Library。
  2. 在 app module 的 build.gradle 文件中添加 dataBinding 元素,如下:

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

这样DataBinding插件就会在你的项目内添加编译和运行时必需的依赖配置。

如果你的 app module 依赖了一个使用 data binding 的库,那么你的 app
module 的 build.gradle 也必须配置 data binding

此外,您使用的 Android Studio 还要支持 DataBinding 特性才行。在 Android
Studio 1.3 以及之后的版本提供了 data binding 的支持,详见 Android
Studio Support for Data
Binding。

总结起来使用 DataBinding 环境要求如下:

  1. Android 2.1(API level 7+) 以上
  2. Android Gradle 插件 1.5.0-alpha1 以上
  3. 最新 Support Library
  4. Android Studio 1.3 以上

  5. Data Binding 中的布局文件入门


属性扩展

BindingMethods 和 BindingAdapter 注解
但是部分 View 的布局属性并没有完整对应的方法提供,比如说 ImageView
"android:tint"布局属性的对应方法是setImageTintList(@Nullable ColorStateList tint),这时就需要使用
DataBinding 提供的处理方法,使用BindingMethods注解

@BindingMethods({
    @BindingMethod(type = android.widget.ImageView.class, attribute = "android:tint", method = "setImageTintList"),
    @BindingMethod(type = android.widget.ImageView.class, attribute = "android:tintMode", method = "setImageTintMode"),
})
public class ImageViewBindingAdapter {
    @BindingAdapter("android:src")
    public static void setImageUri(ImageView view, String imageUri) {
        if (imageUri == null) {
            view.setImageURI(null);
        } else {
            view.setImageURI(Uri.parse(imageUri));
        }
    }

    @BindingAdapter("android:src")
    public static void setImageUri(ImageView view, Uri imageUri) {
        view.setImageURI(imageUri);
    }

    @BindingAdapter("android:src")
    public static void setImageDrawable(ImageView view, Drawable drawable) {
        view.setImageDrawable(drawable);
    }
}

这是系统提供的 ImageViewBindingAdapter,可以在引入了 DataBinding
后全局搜索查看详情,通过BindingMethod注解将两者关联起来,但是如果 View
甚至没有实现对应方法或者需要绑定自定义方法,这是可以使用BindingAdapter注解

BindingConversion 注解
有时在 xml
中绑定的属性,未必是最后的set方法需要的,比如想用color(int),但是 view
需要 Drawable,比如我们想用String,而 view 需要的是 Url
。这时候就可以使用BindingConversion注解

<View
    android:background=“@{isError ? @color/red : @color/white}”
    android:layout_width=“wrap_content”
    android:layout_height=“wrap_content”/>

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

链式表达式

<ImageView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

代码可以优化成

<ImageView android:id=“@+id/avatar”
           android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{avatar.visibility}”/>
<CheckBox android:visibility="@{avatar.visibility}"/>

在系统生成的 Bindinng 类中,会被解析成这三个控件可见性都跟随着
user.isAdult 的状态而改变

android:text="@{user.displayName ?? user.lastName}"代替:android:text="@{user.displayName != null ? user.displayName : user.lastName}"

<import type="com.example.home.data.User" /><import type="com.examle.detail.data.User" alias="DetailUser" /><variable name="user" type="DetailUser" />

2.1. 编写 data binding 表达式

2.1.1 DataBinding 的布局文件与以前的布局文件有一点不同。它以一个 layout
标签作为根节点,里面包含一个 data 标签与 view 标签。view
标签的内容就是不使用 data binding 时的普通布局文件内容。例子如下:

<?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>

2.1.2 在 data 标签中定义的 user
变量,可以在布局中当作属性来使用,用来写一些和java代码中表达式类似的”databinding表达式”

<variable name="user" type="com.example.User"/>

2.1.3 在布局文件中属性值里使用 “@{}”
的语法,来表示”databinding表达式”。结合2.2,这里 TextView 的文本被设置为
user 中的 firstName 属性。其中,user.firstName 和 java
中的表达式含义类似。

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>

使用Callback

另一种情况:*如果import的类为静态类(通常为工具类会用到),我们并不能使用

2.2. 数据对象

刚刚在布局文件中我们使用com.example.User类定义了一个 user
变量,现在我们假设 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;
   }
}

因为成员变量都是final的,所以上面这个User类型的对象拥有不可改变的数据(immutable)。在应用中,这种写一次之后永不变动数据的对象很常见。
这里也可以使用 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() 方法。此外,如果 user 中有 firstName()
方法存在,@{user.firstName}表达式也可以表示对firstName() 方法的调用。

事件绑定

DataBinding
不仅可以在布局文件中为控件绑定数值,也可以在布局文件中为控件绑定监听事件

  • android:onClick
  • android:onLongClick
  • android:onTouch
  • ……

通常会在java代码中定义一个名为Handler或者Presenter的类,然后set进来

<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="android.view.View"/>

        <variable
            name="user"
            type="com.sanousun.sh.databinding.bean.User"/>

        <variable
            name="presenter"
            type="com.sanousun.sh.databinding.activity.SecondActivity.Presenter"/>

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_mobile"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{presenter::mobileClick}"
            android:text="@{user.firstName}"/>

        <TextView
            android:id="@+id/tv_pwd"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{()->presenter.pwdClick()}"
            android:text="@{user.lastName}"/>

    </LinearLayout>

</layout>

在java代码中:

public class Presenter {
    public void mobileClick(View view) {
        Toast.makeText(SecondActivity.this, "mobile click", Toast.LENGTH_LONG).show();
    }

    public void pwdClick() {
        Toast.makeText(SecondActivity.this, "pwd click", Toast.LENGTH_LONG).show();
    }
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mSecondBinding = DataBindingUtil.setContentView(this, R.layout.activity_second);
    mSecondBinding.setUser(new User("da", "shu"));
    mSecondBinding.setPresenter(new Presenter());
}

事件绑定使用 lambda 表达式,绑定形式主要是有两种形式:

<variable name="user" type="DetailUser" />

2.3. 数据绑定

上面工作完成后,数据绑定工具在编译时会基于布局文件生成一个 Binding
类。默认情况下,这个类的名字是基于布局文件的名字产生的,先把布局文件的名字转换成帕斯卡命名形式,然后在名字后面接上”Binding”。例如,上面的那个布局文件叫
main_activity.xml,所以会生成一个 MainActivityBinding
类。这个类中包含了布局文件中所有的绑定关系,并且会根据绑定表达式给布局文件中的
View
属性赋值(user变量和user表达式,view绑定,view数据绑定,view命令绑定)。编译时产生Binding类主要完成了2个事情,1.解析layout文件,根据data标签定义成员变量;2.解析layout文件,根据”databinding表达式”产生绑定代码。Binding
创建好之后还需要,创建 Binding 类的对象,并和view绑定。

2.3.1 在 Activity inflate 一个布局的时候创建 binding,例子如下:

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

就这么简单!运行应用,你会发现测试信息已经显示在界面中了。

DataBindingUtil.setContentView(this, R.layout.main_activity);

这句代码主要做了3件事情,1.把布局设置给Activity,填充为view树;2.创建Binding类对象;3.把view保存在Binding类的成员中,绑定view。这种方式只适合用于Activity中。

2.3.2 也可以通过下面这种方式绑定view:

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

MainActivityBinding.inflate() 会填充 MainActivityBinding
对应的布局,并创建 MainActivityBinding
对象,把布局和MainActivityBinding对象绑定起来。

2.3.3 如果在 ListView 或者 RecyclerView 的 adapter 中使用 data
binding,可以这样写:

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

Method References

需要方法参数及返回值与对应的 listener 一致,在编译时生成对应的
listenerImpl 并在放置 presenter 时为对应控件添加监听,如上面的
mobileClick

// Listener Stub Implementations
public static class OnClickListenerImpl implements android.view.View.OnClickListener{
     private com.sanousun.sh.databinding.activity.SecondActivity.Presenter value;
    public OnClickListenerImpl setValue(com.sanousun.sh.databinding.activity.SecondActivity.Presenter value) {
        this.value = value;
        return value == null ? null : this;
    }

    @Override
    public void onClick(android.view.View arg0) {
        this.value.mobileClick(arg0);
    }
}

代码中会做 presenter 的空判断

因为这条标签name="user"相当生成一个对象了,那么这个对象并不能引用静态类的方法,我应该直接使用就好了:

2.4. 命令绑定

上面演示了数据绑定,下面演示命令绑定,databinding
还允许你编写表达式来处理view分发的事件(比如
onClick)。事件属性名字取决于监听器方法名字,例如View.OnLongClickListener有onLongClick())的方法,因此这个事件的属性是android:onLongClick。

处理事件有两种方法:

  • 方法绑定:在您的表达式中,您可以引用符合监听器方法签名的方法。当表达式的值为方法引用时,Data
    Binding会创建一个监听器,并封装方法引用和方法所有者对象,然后在目标视图上设置该监听器。如果表达式的值为null,DataBinding
    则不会创建侦听器,而是设置一个空侦听器。
  • Lisenter 绑定:如果事件处理表达式中包含lambda表达式。DataBinding
    会创建一个监听器,设置给视图。当事件分发时,侦听器才会计算lambda表达式的值。

Listener Bindings

无需匹配对应 listener
的参数,只需要保证返回值的一致即可(除非是void)。与 Method References
的最大的不同点在于
它是在点击事件发生时相应的

// callback impls
public final void _internalCallbackOnClick(int sourceId , android.view.View callbackArg_0) {
    // localize variables for thread safety
    // presenter
    com.sanousun.sh.databinding.activity.SecondActivity.Presenter presenter = mPresenter;
    // presenter != null
    boolean presenterObjectnull = false;

    presenterObjectnull = (presenter) != (null);
    if (presenterObjectnull) {
        presenter.pwdClick();
    }
}

这个方法会在页面有点击时间时调用,同样也会做空判断

当然你也可以通过@BindingMethods@BindingAdapter进行自定义的扩展

<import type="com.liangfeizc.library.utils.MyStringUtils" /><import type="com.liangfeizc.databinding.utils.MyStringUtils" alias="StringUtils" /><TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{StringUtils.capitalize(user.firstName)}" />

<data >...</data>

2.4.1. 方法绑定

事件可以直接绑定到事件处理器的方法上,类似于android:onClick可以分配一个
Activity 中的方法。与View#onClick属性相比,方法绑定的主要优点是
DataBinding
表达式在编译时就执行过了,因此如果该方法不存在或其签名不正确,您会收到一个编译时错误。

方法绑定和监听器绑定之间的主要区别是,包裹方法引用的监听器实现是在数据绑定时创建的,监听器绑定是在触发事件时创建的。如果您喜欢在事件发生时执行表达式,则应使用监听器绑定。

如果想要将事件处理直接分配给处理程序,那就使用方法绑定表达式,该表达式值是要调用的方法名称。例如,数据对象有如下方法:

public class MyHandlers {
    public void onClickFriend(View view) { ... }
}

绑定表达式就可以像下面这样为视图分配一个点击监听器:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.Handlers"/>
       <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}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

注意,@{handlers::onClickFriend}表达式中onClickFriend的方法签名必须与android:onClick监听器对象中的方法签名完全匹配。

双向绑定

有别于单向绑定使用的@{}符号,双向绑定使用@={}符号用于区别,目前支持的属性有
text,checked,year,month,day,hour,rating,progress 等

主要分两种情况:一种是以数组的形式,如:List<String>,另一种是以链表的形式,如:map<String,String>xml中,要使用<来代替<variable中声明,具体如下:

2.4.2. Lisenter 绑定

Lisenter 绑定是在程序运行中事件发生时才绑定表达式。它和方法绑定类似,但
Listener
绑定允许运行时绑定的任意的数据表达式。此功能适用于版本2.0及更高版本的Android
Gradle插件。 在方法绑定中,方法的参数必须与事件侦听器的参数匹配。
Listener 绑定中,只要返回值与 Lisenter
预期的返回值匹配就行(除非它期望void)

2.4.2.1 例如,您有一个 presenter 类,它具有以下方法:

public class Presenter {
    public void onSaveClick(Task task){}
}

然后,通过lambda表达式您可以将点击事件绑定到您的类中,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
      <variable name="task" type="com.android.example.Task" />
      <variable name="presenter" type="com.android.example.Presenter" />
  </data>
  <LinearLayout 
    android:layout_width="match_parent" 
    android:layout_height="match_parent">
      <Button 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"
        android:onClick="@{() -> presenter.onSaveClick(task)}" />
  </LinearLayout>
</layout>

侦听器只允许 DataBinding
表达式的根元素是lambda表达式。当在表达式中使用回调时,数据绑定自动创建必要的侦听器并且为事件注册。当视图触发事件时,数据绑定才执行给定的表达式。与在正则绑定表达式中一样,在执行这些侦听器表达式时,DataBinding
已经做好了空值和线程安全性的处理。

2.4.2.2 在上面的示例中,我们没有在lambda表达式中定义传递给
onClick(android.view.View)方法的视图参数。侦听器绑定为侦听器参数提供两个选择:1.忽略方法的所有参数;2.命名所有参数。

如果您喜欢命名参数,可以在表达式中使用它们。例如,上面的表达式可以写成:

android:onClick="@{(view) -> presenter.onSaveClick(task)}"

如果你想要使用表达式中的参数,可以在这样使用:

public class Presenter {
    public void onSaveClick(View view, Task task){}
}

android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

您也可以使用具有多个参数的lambda表达式:

public class Presenter {
    public void onCompletedChanged(Task task, boolean completed){}
}

<CheckBox 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content"
    android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

如果正在侦听的事件返回类型不是void类型的值,表达式也必须返回相同类型的值。例如,如果你想监听长点击事件,你的表达式应该返回布尔值。

public class Presenter {
    public boolean onLongClick(View view, Task task){}
}

android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

如果由于空对象而无法计算表达式,数据绑定将返回该类型的默认Java值。例如,引用类型为null,int为0,boolean为false等。

2.4.2.3 如果需要使用带谓词(例如三元)的表达式,则可以使用void作为符号。

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

图片 1

方法绑定和Lisenter绑定的区别.png

总结起来方法绑定和Listener绑定的区别如下

  • 方法引用绑定不能是表达式,Lisenter 绑定可以是表达式;
  • 方法引用绑定在绑定的时候会执行 DataBinding
    表达式,可以自动处理空指针问题,Listener
    绑定在事件触发的时候才会执行 lambda 表达式;
  • 方法引用绑定会限制绑定的方法参数列表,返回值必须和监听器中的方法一致,Listener
    绑定只限制 lambda 表达式中语句的返回值和监听器中的方法一致;
  • 方法引用不止能在 android:onClick=
    这种命令绑定属性中使用,在其他数据绑定属性中也可以使用,而且可以使用表达式作为参数。

<TextView
    android:id="@+id/context_demo"
    android:text="@{user.load(context, @id/context_demo)}" />

public String load(Context context, int field) {
    return context.getResources().getString(R.string.app_name);
}

事件处理除了上述两种方法,还可以直接以数据绑定的形式绑定一个监听器对象(属性setter小节讲解)。

InverseBindingListener

实现双向绑定需要归功于 DataBinding 库中的 InverseBindingListener
接口,这个监听器的作用是监听目标控件的属性改变

private android.databinding.InverseBindingListener mboundView1androidCh = new android.databinding.InverseBindingListener() {
    @Override
    public void onChange() {
        // Inverse of user.male
        //         is user.setMale((boolean) callbackArg_0)
        boolean callbackArg_0 = mboundView1.isChecked();
        // localize variables for thread safety
        // user.male
        boolean maleUser = false;
        // user
        com.sanousun.sh.databinding.bean.User user = mUser;
        // user != null
        boolean userObjectnull = false;
        userObjectnull = (user) != (null);
        if (userObjectnull) {
            user.setMale((boolean) (callbackArg_0));
        }
    }
};

对应 DataBinding 类中有根据双向绑定生成的 Inverse Binding Event Handlers

@Override
protected void executeBindings() {
    ......
    android.databinding.adapters.CompoundButtonBindingAdapter.setListeners(this.mboundView1, (android.widget.CompoundButton.OnCheckedChangeListener)null, mboundView1androidCh);
}

在绑定时,设置到对应的控件中,当监听控件属性改变时,就会触发重绑定,更新属性值

以数组为原型的集合使用:

2.4.3. 避免复杂监听器

Listener表达式非常强大,可以使代码非常容易阅读。另一方面,包含复杂表达式的
Listener
又会使布局难以阅读和难以维护。这些表达式应该保持简单,比如只用来从UI传递可用数据到回调方法一样简单,任何业务逻辑还是应该在从侦听器表达式调用的回调方法中实现。

为了避免冲突,有些点击事件处理程序他们需要一个专门的属性,它们不是android:onClick。databinding已通过@BindingMethods注解(属性setter小节讲解)创建以下属性来避免此类冲突。

Class Listener Setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

InverseBindingMethods 和 InverseBindingAdapter 注解

如果你想做自定义的双向绑定,你必须充分理解这几个注解的含义。

@Target({ElementType.ANNOTATION_TYPE})
public @interface InverseBindingMethod {
    Class type();
    String attribute();
    String event() default ""; // 默认会根据attribute name获取get
    String method() default "";// 默认根据attribute增加AttrChanged
}

以系统定义的 CompoundButtonBindingAdapter 为例

@BindingMethods({
    @BindingMethod(type = CompoundButton.class, attribute = "android:buttonTint", method = "setButtonTintList"),
    @BindingMethod(type = CompoundButton.class, attribute = "android:onCheckedChanged", method = "setOnCheckedChangeListener"),
})
@InverseBindingMethods({
    @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),
})
public class CompoundButtonBindingAdapter {

    @BindingAdapter("android:checked")
    public static void setChecked(CompoundButton view, boolean checked) {
        if (view.isChecked() != checked) {
            view.setChecked(checked);
        }
    }

    @BindingAdapter(value = {"android:onCheckedChanged", "android:checkedAttrChanged"},
            requireAll = false)
    public static void setListeners(CompoundButton view, final OnCheckedChangeListener listener,
            final InverseBindingListener attrChange) {
        if (attrChange == null) {
            view.setOnCheckedChangeListener(listener);
        } else {
            view.setOnCheckedChangeListener(new OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    if (listener != null) {
                        listener.onCheckedChanged(buttonView, isChecked);
                    }
                    attrChange.onChange();
                }
            });
        }
    }
}

双向绑定需要为属性绑定一个监听器,这里就是需要为"android:checked"属性绑定监听器,通过
@InverseBindingMethod(type = CompoundButton.class, attribute =
“android:checked”),databinding 可以通过 checkedAttrChanged 找到
OnCheckedChangeListener,设置 OnCheckedChangeListener 来通知系统生成的
InverseBindingListener 调用 onChange 方法,从而通过 getter
方法来获取值。值得注意的是为了防止无限循环调用,setter
方法必须要去进行重判断

同样如果没有对应方法,可以自定义 InverseBindingAdapter
来实现,详情见系统TextViewBindingAdapter

<variable name="list" type="List<String>" /><TextView android:text="@{list[index]}"/>

3. Data Binding 中的布局文件详解

隐式调用

实现了双向绑定的属性就可以隐式调用,而不用写繁琐的 listener

<CheckBox android:id="@+id/cb"/>
<ImageView android:visibility="@{cb.checked ? View.VISIBLE : View.GONE}"/>

map为原型的键值的集合使用(比较多变,都是一个意思吧)

3.1. import

3.1.1 data标签内可以有0个或者多个 import
标签。这样你可以在布局文件中像在 Java 代码中一样引用这些类。

<data>
    <import type="android.view.View"/>
</data>

导入类以后,在databinding表达式中这个类就可以像在 java
代码中一样使用,比如用来定义变量,比如访问类中的静态方法和属性。例如,现在
View 可以在表达式中如下面这样引用:

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

3.1.2 当类名发生冲突时,还可以使用 alias 重命名:

<import type="android.view.View"/>
<import type="com.example.real.estate.View"
        alias="Vista"/>

现在,Vista 可以用来在布局文件中引用 com.example.real.estate.View
,同时 View 也能被使用,用来引用android.view.View

3.1.3 导入的类型可以用于变量的类型引用和表达式中:

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List<User>"/>
</data>

注意:目前 Android Studio
还没有对导入提供自动补全的支持,但你的应用仍然可以正常被编译。你也可以在变量定义中使用完整的包名来解决这个问题。

<TextView
   android:text="@{((User)(user.connection)).lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

3.1.4 导入的类后,就可以在表达式中使用类的静态属性/方法:

<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"/>

3.1.5 和 Java 一样,java.lang.* 会被自动导入。

属性改变监听

当然我们可以通过 Observable.OnPropertyChangedCallback
来监听属性的改变,从而实现具体的业务逻辑

user.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
    @Override
    public void onPropertyChanged(Observable observable, int i) {
        if (i== BR.firstName){
            Toast.makeText(ThirdActivity.this, user.getFirstName(), Toast.LENGTH_LONG).show();
        }
    }
});
<variable name="map" type="Map<String, String>"/><TextView android:text='@{map["firstName"]}'/><TextView android:text="@{map[`firstName`]}"/><TextView android:text="@{map["firstName"]}" />

3.2. 变量

data 标签中可以有任意数量的 variable 标签。每个 variable 标签描述了会在
binding 表达式中使用的属性。

<data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user"  type="com.example.User"/>
    <variable name="image" type="Drawable"/>
    <variable name="note"  type="String"/>
</data>

在编译时会检查变量的类型,所以如果变量的类型实现了 Observable
接口或者是一个可观察容器类,这些可以被反射检查到。如果变量的类型是没有实现
Observable 接口的类或接口,变量的变动不会引起 UI 的变化!

对于不同配置有不同的布局文件时(比如横屏竖屏的布局),这些布局文件中定义的变量会被合并,所以这些不同配置的布局文件之间不能有冲突的变量定义。

自动生成的 binding 类会为每一个变量产生 getter/setter
函数。这些变量会使用 Java 的默认值,直到 setter 函数被调用。默认值有
null,0(int),false(boolean)等。

binding 类还会生一个命名为 context 的特殊变量,这个变量可以被用于
binding 表达式中。context 变量其实是 rootView 的 getContext()
的返回值。如果在布局文件中自己定义 context 变量,默认的 context
变量会被自己定义的显式 context 变量覆盖。

RecyclerView的处理

只要简单的定义 ViewHolder

public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {

    protected final T mBinding;

    public BindingViewHolder(T binding) {
        super(binding.getRoot());
        mBinding = binding;
    }

    public T getBinding() {
        return mBinding;
    }
}

因为逻辑和属性的绑定在xml中就已经处理好,adapter
的创建变得十分的容易,一般情况下可以直接使用,如果需要额外的更改可以继承。而点击事件的监听可以在
onBindViewHolder 中设置
对于含有多种 viewType 的列表适配器,在不同 xml 布局文件中 variable 的
name 可以全部写为 item,那么在绑定数据时
无需特殊处理

@Override
public void onBindViewHolder(BindingViewHolder holder, int position) {
    final Data data = mData.get(position);
    holder.getBinding().setVariable(item, data);
    holder.getBinding().executePendingBindings();
}

在生成的代码中会去检查它的类型,并将其赋值

  • 格式化字符串(可使用正则表达式等)

3.3. 自定义 Binding 类名

默认情况下,binding
类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加
“Binding” 结尾。这个类会被放置在 databinding 包中。举个例子,布局文件
contact_item.xml 会生成 ContactItemBinding 类。如果 module 包名为
com.example.my.app,binding 类会被放在 com.example.my.app.databinding
中。

3.3.1 通过设置 data 标签中的 class 属性,可以修改 Binding
类的命名与位置。比如:

<data class="ContactItem">
    ...
</data>

会在 databinding 包中生成名为 ContactItem 的 binding 类。

3.3.2 如果需要放置在应用程序的包名下,可以在前面加 “.”,比如:

<data class=".ContactItem">
    ...
</data>

ContactItem 会直接生成在 module 包下。

3.3.3 如果提供完整的包名,binding 类可以放置在任何包名中,比如:

<data class="com.example.ContactItem">
    ...
</data>

高级用法

3.4. Includes

通过使用应用程序命名空间+变量名命名的属性,布局中的变量可以从包含include标签的布局中传递到
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.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>

xmlns:bind="http://schemas.android.com/apk/res-auto" bind
是应用程序命名空间,bind:user user 是变量名。另外需要注意,name.xml 与
contact.xml 中也都需要声明 user 变量。比如 name.xml:

<?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:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textColor="@android:color/holo_red_dark"
            android:text="@{`include : ` + user.firstName}"/>
    </LinearLayout>

</layout>

上面布局中如果 user.firstName 值为 “zhao”,那么 TextView
中就会显示include:zhao

Data binding 不支持 merge 直接包含 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.example.User"/>
   </data>
   <merge>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

component 注入

Data Binding Component详解 –
换肤什么的只是它的一个小应用!

android:text="@{@string/nameFormat(firstName, lastName)}"

3.5. DataBinding表达式语言

原理简述

Author

发表评论

电子邮件地址不会被公开。 必填项已用*标注