MVVM 和 Android Data Binding

在 Android 开发过程中,由于 Android 作为 View 描述的 xml 视图功能较弱,开发中很容易写出臃肿繁杂的 Activity/Fragment,甚至有写出过数千行代码的 Activity。大量的显示、校验、事件响应、回调接口充斥在 Activity 中。Activity/Fragment 实际上成了 View 和 Controller 的混合体,既要承担 View 的显示功能,又要承担 Controller 的控制功能。承担的功能过多,膨胀成繁芜的巨类也就不足为怪了。

UI 界面设计模式

在传统的 UI 设计模式中,MVC 已经实践中证明了其价值,并在漫长的使用过程中((MVC最早出现于 1970 年代)演化出 MVP 和 MVVM 多个变种。

MVC

MVC 我们都知道是 Model-View-Controller,为了使得程序的各个部分分离降低耦合性,MVC 除了把应用程序分成 View、Model 层,还额外的加了一个 Controller 层,它的职责为进行 Model 和 View 之间的协作(路由、输入预处理等)的应用逻辑;Model 进行处理业务逻辑。

MVC Pattern

MVC 通常的处理时序如下:

  1. View 接受用户的交互请求;
  2. View 将请求转交给 Controller;
  3. Controller 操作 Model 进行数据更新;
  4. 数据更新之后,Model 通知 View 数据变化;
  5. View 显示更新之后的数据。

通常 Model 使用 Observer 模式通知 View 数据变化:

MVC with Observer

MVC 的优点:

  • 把业务逻辑和展示逻辑分离,模块化程度高。且当应用逻辑需要变更的时候,不需要变更业务逻辑和展示逻辑,只需要Controller换成另外一个Controller就行了(Swappable Controller)。
  • 观察者模式可以做到多视图同时更新。

MVD 的缺点:

  • Controller 测试困难。因为视图同步操作是由 View 自己执行,而 View 只能在 UI 环境下运行。在没有 UI 环境下对 Controller 进行单元测试的时候,应用逻辑正确性是无法验证的:Model 更新的时候,无法对 View 的更新操作进行断言。
  • View 无法组件化。View 是强依赖特定的 Model 的,如果需要把这个 View 抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的 Model 是不一样的。
  • 当有变化的时候需要同时维护 Model, View, Controller 及其交互,这显然让事情复杂化了。

MVP

为了解决 MVC 的权限,MVP 对 MVC 进行了改良,MVP 模式把 MVC 模式中的 Controller 换成了 Presenter:

MVP Pattern

MVP 通常的调用时序如下:

  1. View 接受用户的交互请求;
  2. View 将请求转交给 Presenter;
  3. Presenter 操作 Model 进行业务处理;
  4. Model 通知 Presenter 数据发生变化;
  5. Presenter 更新 View 的数据。

和 MVC 不同的是,Presenter 会反作用于 View,不像 Controller 只能被动的接受 View 的指挥。

通常我们会抽象 View 接口,暴露属性和事件,然后 Presenter 引用 View 接口。这样可以很容易的构造 View 的 Mock 对象,提高可单元测试性。在这里,Presenter 的责任变大了,不仅要操作数据,而且要更新 View。

上面讲的是 MVP 的 Passive View 模式,该模式下 View 非常 Passive,它几乎什么都不知道,Presenter 让它干什么它就干什么。

在实际的实现中,有人会倾向于 奖 Presenter 一部分简单的同步逻辑交给 View 自己去做,Presenter 只负责比较复杂的、高层次的 UI 操作,所以可以把它看成一个 Supervising Controller,这种模式也被称为 The Supervising Controller MVP:

The Supervising Controller MVP

MVP 的优点:

  • 便于测试。Presenter 对 View 是通过接口进行,在对 Presenter 进行不依赖 UI 环境的单元测试的时候。可以通过 Mock 一个 View 对象,这个对象只需要实现了 View 的接口即可。
  • View 可以进行组件化。在 MVP 当中,View 不依赖 Model。这样就可以让 View 从特定的业务场景中脱离出来,可以说 View 可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的 View组件。

MVP 缺点:

  • Presenter 中除了应用逻辑以外,还有大量的 View->Model,Model->View 的手动同步逻辑,造成 Presenter 比较笨重,维护起来会比较困难。

MVVM

MVVM 可以看作是一种特殊的 MVP(Passive View)模式,或者说是对 MVP 模式的进一步改良。

MVVM 模式最早是微软公司提出,并且了大量使用在.NET的WPF和Sliverlight中。2005年微软工程师John Gossman在自己的博客上首次公布了MVVM模式。

MVVM Pattern

MVVM 代表的是 Model-View-ViewModel。MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致,唯一区别在于 ViewModel 将密切关联的 Model 和 View 的逻辑单独提取出来,用数据绑定将他们关联到一起。Model 的改变会通过 ViewModel 来映射到 View 上,反之亦然。数据绑定你可以认为是 Observer 模式或者是 Publish/Subscribe 模式,原理都是为了用一种统一的集中的方式实现频繁需要被实现的数据更新问题。

比起MVP,MVVM 不仅简化了业务与界面的依赖关系,还优化了数据频繁更新的解决方案,甚至可以说提供了一种有效的解决模式。

MVVM 优点:

  • 省去了model变化之后手动修改view和view变化之后手动修改model的繁琐工作;
  • UI和功能更加松耦合了,功能的可测试性就越来越强。

MVVM 缺点:

  • 在复杂的情况下,很难预先设计好足够通用的 ViewModel;
  • 通常会依赖于特定的数据绑定框架;

Android Data Binding

2015 Google IO 大会带来的 Data Binding 库使得 Android 开发者可以方便的实现 MVVM 架构模式。

警告:Data Binding 库目前还是 Beta 版本,采用需谨慎。


以下大部分内容摘录自Data Binding(数据绑定)用户指南,根据 Android 官方英文指南 做了一些更新。

配置环境

最新版的 Android Studio 已经内置了对 Android Data Binding 框架的支持,配置起来也很简单,只需要在 app 的 build.gradle 文件中添加下面的内容就好了

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

Data Binding Layout 文件

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内描述了一个名为user的变量属性,使其可以在这个layout中使用:

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

在layout的属性表达式写作 @{},下面是一个 TextView 的 text 设置为 user 的 firstName 属性:

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

Data Object

假设你有一个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() 方法。

绑定数据

默认情况下,一个 Binding 类会基于 layout 文件的名称而产生,将其转换为 Pascal case(译注:首字母大写的命名规范)并且添加 “Binding” 后缀。上述的 layout 文件是 activity_main.xml,因此生成的类名是 ActivityMainBinding。此类包含从 layout 属性到 layout 的 Views 中所有的 bindings(例如user变量),并且它还知道如何给 Binding 表达式分配数值。创建 bindings 的最简单的方式是在 inflating(译注:layout文件与Activity/Fragment的“链接”)期间如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
}

就是这样,运行 app 后,你将会看到 Test 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);

绑定事件

就像你可以在xml文件里面使用属性android:onClick绑定Activity里面的一个方法一样,Data Binding Library 扩展了更多的事件可以用来绑定方法,比如 View.OnLongClickListener 有个方法 onLongClick(), 你就可以使用 android:onLongClick 属性来绑定一个方法,需要注意的是绑定的方法的签名必须和该属性原本对应的方法的签名完全一样,否则编译阶段会报错。

下面举例来说明具体怎么使用,先看用来绑定事件的类:

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

    public void afterFirstNameChanged(Editable s) { ... }
}

然后就是layout文件:

<?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">
        <EditText android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}"
            android:afterTextChanged="@{handlers.afterFirstNameChanged}"/>
        <Button android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{handlers.onClickButton}"/>
    </LinearLayout></layout>

深入 Layout 文件

Import

零个或多个 import 元素可能在 data 元素中使用。这些只用在你的 layout 文件中添加引用,就像在 Java 中:

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

现在,View 可以使用你的 Binding 表达式:

<TextView
   android:text="@{user.lastName}"
   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.example.real.estate.View"
        alias="Vista"/>

这样,在该 layout 文件中 Vista 对应 com.example.real.estate.View,而View对应android.view.View。导入的类型可以在Variable和表达式中使用作为引用来使用:

<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还没有处理imports,所以自动导入Variable在你的IDE不能使用。您的app仍会正常编译,你可以在您的Variable定义中使用完全符合规定的名称来解决该IDE问题。

<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"/>
就像在Java中,java.lang。*是自动导入的。

Variables

在data中可以使用任意数量的variable元素。每一个variable元素描述了一个用于layout文件中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>

该Variable类型在编译时检查,因此如果一个Variable实现了Observable或observable collection,这应该反映在类型中。(译注:需要查找资料来理解)如果variable是一个没有实现Observable接口的基本类或者接口,Variables不会被observed!

当对于多种配置有不同的layout文件时(如,横向或纵向),Variables会被合并。这些layout文件之间必须不能有冲突的Variable定义。

产生的Binding类对于每一个描述的Variables都会有setter和getter。这些Variables会使用默认的Java值 - null(引用类型)、0(int)、false(boolean)等等,直到调用setter时。

自定义 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>

Includes

通过使用application namespace以及在属性中的Variable名字从容器layout中传递Variables到一个被包含的layout:

<?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两个layout文件中必需要有user variable

Data binding 不支持包含 merge 元素作为直接的子元素,比如以下layout是不支持的:

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

表达式语言

常用表达式跟Java表达式很像,以下这些是一样的:

  • 数学表达式 + – / * %
  • 字符串链接 +
  • 逻辑操作符 && ||
  • 二元操作符 & | ^
  • 一元操作符 + – ! ~
  • Shift >> >>> <<
  • 比较 == > < >= <=
  • instanceof
  • Grouping ()
  • Literals – character, String, numeric, null
  • Cast
  • 函数调用
  • 值域引用(Field access)
  • 通过[]访问数组里面的对象
  • 三元操作符 ?:

示例:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

缺少的操作:

  • this
  • super
  • new
  • 显式泛型调用

Null合并操作

?? - 左边的对象如果它不是null,选择左边的对象;或者如果它是null,选择右边的对象:

android:text="@{user.displayName ?? user.lastName}"

函数上的写法如下:

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

属性引用

我们已经在前边“Data Binding表达式”中提到了JavaBean引用的简短格式。

当一个表达式引用一个类的属性,它仍使用同样的格式对于字段、getters以及ObservableFields。

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

避免 NullPointerException

Data Binding代码生成时自动检查是否为nulls来避免出现null pointer exceptions错误。例如,在表达式@{user.name}中,如果user是null,user.name会赋予它的默认值(null)。如果你引用user.age,age是int类型,那么它的默认值是0。

集合

常用的集合: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]}"

字符串

当使用单引号包含属性值时,在表达式中使用双引号很容易:

android:text='@{map["firstName"]}'

使用双引号来包含属性值也是可以的。字符串前后需要使用"`":

android:text="@{map[`firstName`]}"
android:text="@{map["firstName"]}"

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)}"

一些资源需要显式类型判断:

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 Object

任何Plain old Java object(PO​​JO)可用于Data Binding,但修改POJO不会导致UI更新。Data Binding的真正能力是当数据变化时,可以通知给你的Data对象。有三种不同的数据变化通知机制:Observable对象、ObservableFields以及observable collections。

当这些可观察Data对象​​绑定到UI,Data对象属性的更改后,UI也将自动更新。

Observable 对象

实现android.databinding.Observable接口的类可以允许附加一个监听器到Bound对象以便监听对象上的所有属性的变化。

Observable接口有一个机制来添加和删除监听器,但通知与否由开发人员管理。为了使开发更容易,一个BaseObservable的基类为实现监听器注册机制而创建。Data实现类依然负责通知当属性改变时。这是通过指定一个Bindable注解给getter以及setter内通知来完成的。

private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getFirstName() {
       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);
   }
}

在编译期间,Bindable注解在BR类文件中生成一个Entry。BR类文件会在模块包内生成。如果用于Data类的基类不能改变,Observable接口通过方便的PropertyChangeRegistry来实现用于储存和有效地通知监听器。

Observable 字段

一些小工作会涉及到创建Observable类,因此那些想要节省时间或者几乎没有几个属性的开发者可以使用ObservableFields。ObservableFields是自包含具有单个字段的observable对象。它有所有基本类型和一个是引用类型。要使用它需要在data对象中创建public final字段:

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

就是这样,要访问该值,使用set和get方法:

user.firstName.set("Google");
int age = user.age.get();

在实践过程中,有时 notifyPropertyChanged(BR.lastName); 很容易引用错误,因此,开发过程中还是推荐使用 ObservableField。

Observable 集合

一些app使用更多的动态结构来保存数据。Observable集合允许键控访问这些data对象。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"/>

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

生成 Binding

Binding类的生成链接了layout中variables与Views。如前所述,Binding的名称和包名可以定制。所生成的Binding类都扩展了android.databinding.ViewDataBinding。

创建

Binding应在inflation之后就立马创建,以确保View层次结构不在之前打扰layout中的binding到views上的表达式。有几个方法可以绑定到一个layout。最常见的是在Binding类上使用静态方法.inflate方法载入View的层次结构并且绑定到它只需这一步。还有一个更简单的版本,只需要LayoutInflater还有一个是采用ViewGroup:

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(LayoutInflater, viewGroup, false);

如果使用不同的机制载入layout,他可一分开绑定:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有时Binding不能提前知道,对于这种情况,可以使用DataBindingUtil类来创建Binding:

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

带ID的Views

在layout中对于每个带ID的View会生成一个public final字段。Binding在View层次结构上做单一的传递,提取带ID的Views。这种机制比起某些Views使用findViewById还要快。例如:

<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}"
   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;

IDs不像没有Data Bindings那样几乎没有必要,但是仍然会有一些实例需要从代码中访问Views。

Variables

每个Variable会有访问方法。

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

它会在Binding中生成setters和getters:

public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);

Binding进阶

动态Variables

有时,不知道具体的Binding类,例如,一个RecyclerView适配器对layouts任意操作并不知道具体的Binding类。它仍然必需在onBindViewHolder期间赋值给Binding。

在这个例子中,该RecyclerView绑定的所有layouts有一个“item”的Variable。该BindingHolder有一个getBinding方法返回ViewDataBinding。

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

直接Binding

当一个variable或observable变化时,binding会在计划在下一帧之前执行改变。可能会发生很多次,但是在Binding时必须立即执行。要强制执行,使用executePendingBindings()方法。

后台线程

只要它不是一个集合,你可以在后台线程中改变你的数据模型。在判断是否要避免任何并发问题时,Data Binding会对每个Varialbe/field本地化。

属性Setters

每当绑定值的变化,生成的Binding类必须调用setter方法。Data Binding 框架有可以自定义赋值的方法。

自动Setters

对于一个属性,Data Binding试图找到setAttribute方法。与该属性的namespace并不什么关系,仅仅与属性本身名称有关。

例如,有关TextView的android:text属性的表达式会寻找一个setText(String)的方法。如果表达式返回一个int,Data Binding会搜索的setText(int)方法。注意:要表达式返回正确的类型,如果需要的话使用casting。Data Binding仍会工作即使没有给定名称的属性存在。然后,您可以通过Data Binding轻松地为任何setter“创造”属性。例如,DrawerLayout没有任何属性,但有大量的setters。您可以使用自动setters来使用其中的一个。

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

重命名的Setters

一些有setters的属性按名称并不匹配。对于这些方法,属性可以通过BindingMethods注解相关联。这必须与一个包含BindingMethod注解的类相关联,每一个用于一个重命名的方法。例如,android:tint属性与setImageTintList相关联,而不与setTint相关。

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

以上例子,开发者需要重命名setters是不太可能了,android架构属性已经实现了。

自定义Setters

有些属性需要自定义绑定逻辑。例如,对于android:paddingLeft属性并没有相关setter。相反,setPadding(left, top, right, bottom)是存在在。一个带有BindingAdapter注解的静态绑定适配器方法允许开发者自定义setter如何对于一个属性的调用。

Android的属性已经创造了BindingAdapters。举例来说,对于paddingLeft:

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

Binding适配器对其他定制类型非常有用。例如,自定义loader可以用来异步载入图像。

当有冲突时,开发人员创建的Binding适配器将覆盖Data Binding默认适配器。

您也可以创建可以接收多个参数的适配器。

@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}”/>

如果对于一个ImageViewimageUrl和error都被使用并且imageUrl是一个string类型以及error是一个drawable时,该适配器会被调用。

  • 匹配的过程中自定义namespaces将被忽略。
  • 你也可以为Android namespaces写适配器。

Binding适配器方法可能从handlers中获取旧的属性值. 同时获取新旧属性值的方法应该把旧的属性值作为参数放在前边,紧跟着是新的属性值:

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

事件处理器可仅在接口或者抽象类中使用,如:

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

当监听器有多个方法时,必须被拆分成多个监听器。如 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);
}

因为改变某个监听器会影响到其他的监听器,我们必须编码三个不同的binding适配器,为每个属性各编写一个,并同时为两者一起编写一个,他们必须同时被设置。

@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 使用一个 set 方法。android.databinding.adapters.ListenerUtil 类保持对之前所有监听器的追踪,所以,他们必须从绑定适配器中移除。

通过对接口 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 用 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 进行注解, 数据绑定代码生成器明白监听器仅需在运行 Honeycomb 及以上版本的设备上生成,
addOnAttachStateChangeListener(View.OnAttachStateChangeListener) 支持同样的版本.

转换

对象转换

当从Binding表达式返回一个对象,一个setter会从自动、重命名以及自定义的setters中选择。该对象将被转换为所选择的setter的参数类型。

这是为了方便那些使用ObservableMaps来保存数据。例如:

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

在userMap返回一个对象并且该对象将自动转换为setText(CharSequence)的参数类型。当有关参数类型可能混乱时,开发人员需要在表达式中转换。

自定义转换

有时候转换应该是自动的在特定类型之间。例如,设置背景的时候:

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

这里,背景需要Drawable对象,但颜色是一个整数。不管何时有Drawable并且返回值是一个整数,那么整数类型会被转换为ColorDrawable。这个转换是通过使用带有BindingConversion注解的静态方法完成的:

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

注意:转换仅仅发生在setter级别,因此它不允许以下混合类型:

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Android Studio 对数据绑定的支持

Android Studio 支持数据绑定表达式的语法高亮,并可在编辑器中标示表达式语法错误。

预览窗格可显示数据绑定表达式的预设默认值,在下面的例子中,预览窗格在 TextView 中显示默认的 PLACEHOLDER 文本值。

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

如果你需要在设计期间显示默认值,你也可以使用 tools:attributes 代替默认表达式值。

参考资料

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

推荐阅读更多精彩内容