Android Data binding官方指南

  • 1. 布局和绑定表达式
    • 1.1 数据对象
    • 1.2 绑定数据
    • 1.3 表达式语言
      • 1.3.1 共同的特征
      • 1.3.2 不支持的操作符
      • 1.3.3 合并null操作符
      • 1.3.4 属性引用
      • 1.3.5 NullPointerException处理
      • 1.3.6 集合
      • 1.3.7 字符串常量引用
      • 1.3.8 资源
  • 2. 事件处理
    • 2.1 方法引用
    • 2.2 监听器绑定
  • 3. 导入、变量和包含
    • 3.1 导入
    • 3.2Variables
    • 3.3 include
  • 4. 可观察数据对象
    • 4.1 可观察对象(Observable Objects)
    • 4.2 可观察属性(ObservableFields)
    • 4.3 可观察集合(Observable Collections)
  • 5. 生成Binding类
    • 5.1 创建绑定对象
    • 5.2 View中使用id
    • 5.3 Variables
    • 5.4 ViewStubs
    • 5.5 立即绑定
    • 5.6 高级绑定
      • 5.6.1 动态Variable
    • 5.7 后台线程
    • 5.8 自定义绑定类名
  • 6. Binding adapters
    • 6.1 设置属性
      • 6.1.1 自动Setters
      • 6.1.2 指定一个自定义方法名
      • 6.1.3 提供自定义逻辑
    • 6.2 转换器
      • 6.2.1 自动对象转换
      • 6.2.2 自定义转换
  • 7. 绑定布局视图到架构组件
    • 7.1 使用LiveData通知UI数据的变化
    • 7.2 使用ViewModel管理UI相关的数据
    • 7.3 使用Observable的ViewModel对binding adapters进行更多的控制
  • 8.双向数据绑定
    • 8.1 使用自定义属性的双向数据绑定
    • 8.2 转换器
    • 8.3 双向数据绑定中的无限循环
    • 8.4 双向属性
    • 8.5 附加资源

1. 布局和绑定表达式

表达式语言允许您编写处理由视图分发的事件的表达式。数据绑定库自动生成将布局中的视图与数据对象绑定的类。

数据绑定布局文件稍有不同,以布局的根标记开始,后面跟着数据元素和视图根元素。此视图元素是您的根将在非绑定布局文件中的内容。下面的代码显示了一个示例布局文件:

(1)布局文件这样写


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

数据中的user变量描述了可以在该布局中使用的属性。

"user" type="com.example.User" />

布局中的表达式使用“@ {}”语法写入属性。这里,TextView文本被设置为用户变量的firstName属性:

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

注意:布局表达式应该保持小而简单,因为它们不能进行单元测试,并且IDE支持有限。为了简化布局表达式,可以使用自定义绑定适配器。

1.1 数据对象

这里假设你有一个普通的对象用于描述User实体:

KOTLIN

data class User(val firstName: String, val lastName: String)

JAVA

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}

在数据绑定中,访问@{user.firstName},那么默认就会访问同名的属性firstName,或对应的getFirstName方法。

这种类型的对象具有从不改变的数据。在应用中常见的数据是一次读取,此后不改变。也可以使用遵循一组约定的对象,如Java中的访问器方法的使用,如下面的示例所示:

KOTLIN

// Not applicable in Kotlin.
data class User(val firstName: String, val lastName: String)

JAVA

1.2 绑定数据

默认情况下,绑定类将基于布局文件的名称来产生
如布局文件为main_activity.xml,这样生成的类是 MainActivityBinding
如下设置Activity的contentView:

KOTLIN

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding: ActivityMainBinding = DataBindingUtil.setContentView(
            this, R.layout.activity_main)

    binding.user = User("Test", "User")
}

JAVA

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

上文说,在xml中用表达式可以获取数据,那么同样的,在代码中也可以设置数据,如这里的:binding.setUser(user),还可以使用inflate的方式来获取生成数据绑定类:

KOTLIN

val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())

JAVA

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

如果是在一个adapter中来使用,例如Fragment, ListView, or RecyclerView 内部的adapter,

你可能倾向于使用bindings类的inflate()方法或者DataBindingUtil类,如下样例代码所示:

KOTLIN

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

JAVA

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

1.3 表达式语言

1.3.1 共同的特征

表达式语言看起来很像Java表达式。如下这些都是相同的:

Mathematical + - / * %
String concatenation +
Logical && ||
Binary & | ^
Unary + - ! ~
Shift >> >>> <<
Comparison == > < >= <=
instanceof
Grouping ()
Literals - character, String, numeric, null
Cast
Method calls
Field access
Array access []
Ternary operator ?:

例:

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

1.3.2 不支持的操作符

thisnewsuper 如这些,还是只能在java类中使用

1.3.3 合并null操作符

如下使用 “??” 操作符

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

等价于

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

1.3.4 属性引用

表达式可以通过如下格式引用一个类的属性,字段,getters,和ObservableField对象与此相同,如下,user有getLastName(),可以简单引用:

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

1.3.5 NullPointerException处理

数据绑定代码会自动检查null值,避免NullPointerException。例如String类型默认为null, Int类型默认0.

1.3.6 集合

一般的集合都可以使用”[]”操作符: arrays, lists, sparse lists, and maps。例

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List"/>
    <variable name="sparse" type="SparseArray"/>
    <variable name="map" type="Map"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

1.3.7 字符串常量引用

如下,可以使用单引号在属性值上,用双引号在字符串字面值上:

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

也可以反过来

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

1.3.8 资源

可以使用正常的访问资源的表达式语法:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

可以为格式字符串和复数提供参数:

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

一些资源需要显示的引用

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

2. 事件处理

数据绑定允许您编写处理从视图中分发的事件的表达式(例如,onClick()方法)。事件属性名称由侦听器方法的名称来确定,但有少数例外。例如,View.OnClickListener有一个方法onClick(),所以这个事件的属性是android:onClick。

有一些用于click事件的专用事件处理程序,它们需要除android:onClick之外的属性以避免冲突。可以使用以下属性来避免这些类型的冲突:

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

您可以使用以下机制来处理事件:

  • 方法引用:在表达式中,可以引用符合侦听器方法签名的方法。当表达式计算为方法引用时,数据绑定将侦听器中的方法引用和所有者对象封装起来,并将该侦听器设置在目标视图上。如果表达式计算为NULL,则数据绑定不创建侦听器,并设置空侦听器。

  • 侦听器绑定:这些lambda表达式是在事件发生时被计算的。数据绑定总是创建一个监听器,监听器设置在视图上。当事件被分发时,侦听器调用lambda表达式。

2.1 方法引用

比如定义一个符合OnClickListener#onClick(View view)方法参数签名规则的方法:

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

如下在xml中绑定并使用:


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

2.2 监听器绑定

表达式使用的方法就可以定义的比较随意了,不用像”方法引用”那样遵循特定规则,如一个方法

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

xml中使用它


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

像上面,我们并没有定义onClick(android.view.View) 参数view。监听器绑定,允许我们忽略所有的参数。如果想要使用参数,可以忽略参数类型,随意定义一个名字。监听器绑定还有很多灵活的写法,可以参考官方文档。

3. 导入、变量和包含

数据绑定库提供了诸如导入、变量和包含的特征。导入使布局文件中的类易于引用。变量允许您描述可以在绑定表达式中使用的属性。包括让您重用应用程序中的复杂布局。

3.1 导入

导入允许您轻松地在布局文件中引用类,就像托管代码一样。可以在数据元素内部使用零个或多个导入元素。下面的代码示例将View类导入布局文件:

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

导入View类允许您从绑定表达式中引用它。下面的示例演示如何引用View类的VISIBLE 和 GONE常量:

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

类型重命名

当类名过长或者有类名称冲突,其中一个类可以重命名一个别名

type="com.grandstream.gsmarket.R.string" alias="Strings"/>

type="android.view.View"/>
type="com.example.real.estate.View"
        alias="EsView"/>

其他类引用

导入类型可以用作变量和表达式中的类型引用。下面的示例显示用户和列表作为变量的类型:

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

警告:Android Studio尚不支持自动补全imports,因此变量导入的自动补全在IDE中可能无法工作。您的应用程序仍然可以编译,并且可以通过在变量定义中使用完全限定名来解决IDE问题。

还可以使用强制类型转换作为表达式的一部分。下面的示例将连接属性强制转换为用户类型:

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

静态类引用

也可以在表达式中运用静态属性或方法,使用MyStringUtils.capitalize():

<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.Lang.*是自动导入的。

示例代码

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >
    <data>

        <variable
            name="viewmodel"
            type="com.grandstream.gsmarket.appdetail.AppDetailItemViewModel" />

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

        <import type="com.grandstream.gsmarket.R.string" alias="Strings"/>
    data>

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <android.support.v7.widget.AppCompatTextView
                        android:id="@+id/download_count_text"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="15dp"
                        android:singleLine="true"
                        android:textColor="#999999"
                        android:textSize="36px"
                        android:text='@{viewmodel.getContext().getString(Strings.download) + viewmodel.downloadCount}'/>
                LinearLayout>

3.2Variables

可以在数据元素内部使用多个变量元素。每个变量元素描述了可以在布局上设置的属性,用于布局文件中的表达式的绑定。下面的示例声明user, image, 和 note变量:

<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,那就需要在type中声明;如果不是一个Observable的实现,那么该variable就不会被观察到.

当对于各种配置(例如,横向或纵向)存在不同的布局文件时,将组合这些变量。在这些布局文件之间不存在冲突的变量定义。

所生成的绑定类对于所描述的变量中的每一个都具有setter和getter。变量采用默认托管代码值(null作为引用类型,0作为int,false作为boolean等等),直到setter被调用。

生成一个名为context的特殊变量,以便根据需要在绑定表达式中使用。context的值是从根视图的getContext()方法中的Context对象。context变量由具有该名称的显式变量声明重写。

<data>

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

        <variable
            name="view"
            type="com.grandstream.gsmarket.apps.ApkListFragment" />

        <variable
            name="viewmodel"
            type="com.grandstream.gsmarket.apps.AppsViewModel" />

    </data>

3.3 include

variables可以传递给使用了include标签的子布局中,如


<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和contract.xml中都可以使用变量user了

不支持在merge下直接使用include的情形。

4. 可观察数据对象

可观测性是指对象能够通知他人其数据变化的能力。数据绑定库允许您使对象、字段或集合可观察。

任何POJO对象都可以用于数据绑定,但是更改POJO对象,并不会引起UI更新。有三种不同的数据更改通知机制:观察对象,观察字段和观察集合。当其中一个绑定到用户界面的可观察的数据对象,观察到数据对象的属性变化,用户界面将自动更新。

4.1 可观察对象(Observable Objects)

类实现 Observable接口可以实现被观察,将被允许附加一个listener,来监听对象所有属性的改变

Observable interface有一个机制来添加和删除listeners,但需要通知开发者。为了让开发变得更容易,提供一个基类,BaseObservable,它实现创建listener的注册机制。当属性数据改变时,数据类实现者需要响应通知。这是通过在getter上使用一个注解@Bindable,并在setter中调用notifyPropertyChanged()进行通知。

KOTLIN

class User : BaseObservable() {

    @get:Bindable
    var firstName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.firstName)
        }

    @get:Bindable
    var lastName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.lastName)
        }
}

JAVA

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

@Bindable 在编译时将生成一个BR class,包含了数据绑定使用的资源IDs ,BR类文件将生成在moudle的package下。如果数据类的基类不能被改变,Observable interface的实现类可以使用PropertyChangeRegistry存储和有效地通知listeners。

4.2 可观察属性(ObservableFields)

如果想做比创建上面的观察对象更少的工作,那么可以使用ObservableField 和它的同级的一些类型:
ObservableBoolean,
ObservableByte,
ObservableChar,
ObservableShort,
ObservableInt,
ObservableLong,
ObservableFloat,
ObservableDouble, and
ObservableParcelable。

它们都是一个包含单一属性的可观察的对象。为避免装箱、拆箱操作,可以在数据类中定义成 public final field …

KOTLIN

class User {
    val firstName = ObservableField()
    val lastName = ObservableField()
    val age = ObservableInt()
}

JAVA

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

如下,使用get或set来获取和设置属性值:

KOTLIN

user.firstName = "Google"
val age = user.age

JAVA

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

注意:Android Studio 3.1和更高版本允许您用LiveData对象替换可观察字段,这对您的应用程序提供了额外的好处。有关更多信息,请参见Use LiveData to notify the UI about data changes。

4.3 可观察集合(Observable Collections)

一些应用程序使用更多的动态结构来保存数据。可观察集合允许通过key访问这些数据对象。当key是一个引用类型,如String,就可以用ObservableArrayMap。

KOTLIN

ObservableArrayMap().apply {
    put("firstName", "Google")
    put("lastName", "Inc.")
    put("age", 17)
}

JAVA

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在下面的布局中,就可以通过String类型的key来访问map中的数据:

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap"/>
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"/>

当key是一个Integer时,可用ObservableArrayList:

KOTLIN

ObservableArrayList().apply {
    add("Google")
    add("Inc.")
    add(17)
}

JAVA

ObservableArrayList user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17); 
  

布局中使用ObservableArrayList:

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

5. 生成Binding类

生成的绑定类,会链接布局的variables以及Views。正如前面所讨论的,绑定的名称和包可以是自定义的。生成的所有绑定类都 extends ViewDataBinding。

为每个布局文件生成绑定类。默认情况下,类的名称基于布局文件的名称,将其转换为Pascal大小写,并向其添加绑定后缀。上面的布局文件名是activity_main.xml,因此相应的生成类是ActivityMainBinding。这个类保存从布局属性(例如,user变量)到布局视图的所有绑定,并且知道如何为绑定表达式分配值。

5.1 创建绑定对象

如前文所述,由相应的布局文件,而生成了相应的binding class。代码中使用它,需要LayoutInflater,如:

KOTLIN

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
    //或者
    val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater,viewGroup, false)
}

JAVA

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
//或者
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);

如果布局已经inflate了,在某些场景时,可以单独绑定:

KOTLIN

val binding: MyLayoutBinding = MyLayoutBinding.bind(viewRoot)

JAVA

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有时binding class 不能提前知道。在这种情况下,可以使用DataBindingUtil类创建绑定:

KOTLIN

val rootView = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)

JAVA

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

如果您在Fragment, ListView, or RecyclerView的适配器中使用数据绑定项,那么您可能更喜欢使用bindings类或DataBindingUtil类的inflate()方法,如下面的代码示例所示:

KOTLIN

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

JAVA

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

5.2 View中使用id

在布局中的每个使用了id的view,都会在binding class中创建出一个同名的public属性。这种绑定机制,比findViewById更快速。

"wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
   android:id="@+id/firstName"/>

在Activity或者Fragment中可以这样引用,binding.firstName

5.3 Variables

数据绑定库为布局中声明的每个变量生成访问器方法。例如,下面的布局在绑定类中为user, image, 和 note变量生成setter和getter方法:

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

5.4 ViewStubs

与普通视图不同,ViewStub对象作为不可见的视图的根节点。当它们变得可见或者被明确地告知要展示时,它们用另一个布局来替换布局中的自己。

因为ViewStub基本上从视图层次结构中消失,绑定对象中的视图也必须消失,以允许被垃圾回收。因为视图是final类型,所以一个ViewStubProxy对象代替生成的绑定类中的ViewStub,当ViewStub存在时,它允许您访问ViewStub,当ViewStub已经inflated时,还允许您访问inflated的视图层次结构。

当inflate另一个布局时,必须为新布局建立绑定。因此,ViewStubProxy必须监听ViewStub OnInflateListener ,并在需要时建立绑定。因为给定时间只能存在一个侦听器,所以ViewStubProxy允许您设置OnInflateListener,在建立绑定之后调用它。

5.5 立即绑定

当一个variable or observable发生了改变,绑定框架会安排在下一帧进行视图的改变。然而,有时希望立即发生改变。可以使用executePendingBindings()来强制执行

5.6 高级绑定

5.6.1 动态Variable

有时,特定的绑定类不会为人所知。例如,一个RecyclerView.Adapter操作的任意布局,不知其特定的绑定类。仍需要通过onBindViewHolder(VH, int)来绑定值。
在下面的例子中,RecyclerView中的所有布局,都绑定了一个item,有一个BindingHolder#getBinding()返回ViewDataBinding 类型:

KOTLIN

override fun onBindViewHolder(holder: BindingHolder, position: Int) {
    item: T = mItems.get(position)
    holder.binding.setVariable(BR.item, item);
    holder.binding.executePendingBindings();
}

JAVA

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

注意:数据绑定库在模块包中生成名为BR的类,该类包含用于数据绑定的资源的ID。在上面的示例中,库自动生成BR.item 变量。

5.7 后台线程

你可以改变你的数据模型在一个后台线程,只要它不是一个集合。数据绑定框架将本地化每个变量和属性,以避免任何并发问题。

5.8 自定义绑定类名

默认情况下,基于布局文件的名称而生成绑定类,开头大写,然后移除下划线,再加上后缀”Binding”。这个类将被放置在一个数据绑定包中,该包位于moudle包下。例如, 布局文件contact_item.xml将生成ContactItemBinding。如果moudle的包名为com.example.my.app, 那么它将被放置在com.example.my.app.databinding下,通过调整data元素的class属性,绑定类可以重命名或放置在不同的包。

"ContactItem">
    ...

这会生成在moudle下的databinding包下,如果要生成moudle包下,可以使用”.”前缀:

".ContactItem">
    ...

在下面这种情况下,ContactItem直接生成在模块包下。可以使用任何完整的包路径:

"com.example.ContactItem">
    ...

6. Binding adapters

绑定适配器负责进行适当的框架调用用于设置值。一个例子是设置属性值,如调用setText()方法。另一个例子是设置事件侦听器,比如调用setOnClickListener()方法。

数据绑定库允许您指定用于设置值的调用方法,提供您自己的绑定逻辑,以及使用适配器指定返回的对象的类型。

6.1 设置属性

当view使用了绑定表达式,只要绑定值发生变化,生成的绑定类必须调用相应的setter方法。定制数据绑定框架的方式方法调用设置值。数据绑定框架允许自定义setter方法。

6.1.1 自动Setters

对于一个attribute,数据绑定框架将试图查找对应的setAttribute()。它的namespace并不重要,只关注attribute的name。如,TextView中的属性android:text使用了android:text=”@{user.name}”表达式,那么框架就会查找setText(String)。如果表达式返回的是int,那么将会查找setText(int)。所以,表达式需要返回正确的类型。数据绑定框架,支持创建一个布局元素(View|ViewGroup)中,并不存在的属性。
如下,生成的binding class中,将生成一个setDrawerListener(listener):

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

6.1.2 指定一个自定义方法名

有些属性具有与名称不匹配的setters。在这些情况下,可以使用BindingMethods注解将属性与setter关联。注解与类一起使用,可以包含多个BindingMethod注解,每个重命名方法都有一个注解。绑定方法的注解是可以添加到应用程序中任何类。在下面的示例中,android:tint属性与setImageTintList(ColorStateList)方法相关联,而不是与setTint()方法相关联:

KOTLIN

@BindingMethods(value = [
    BindingMethod(
        type = android.widget.ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList")])

JAVA

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

大多数时候,你不需要在Android框架类中重命名setters。这些属性已经使用name规则 自动查找匹配方法。

6.1.3 提供自定义逻辑

一些属性需要自定义绑定逻辑。例如,属性android:paddingLeft没有对应的setter方法,而在view中有一个方法为setPadding(left, top, right, bottom)。可以使用@BindingAdapter来自定义一个关于属性android:paddingLeft的setter。例:
KOTLIN

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

JAVA

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

参数类型是重要的。第一个参数确定与属性关联的view的类型。第二个参数确定给定属性的绑定表达式中接受的类型。

绑定适配器对于其他类型的定制是有用的。例如,可以从工作线程中调用自定义的loader来加载图像。

当存在冲突时,您定义的绑定适配器重写由Android框架提供的默认适配器。

您还可以拥有接收多个属性的Binding adapter,如下面的示例所示:

KOTLIN

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

JAVA

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

对应的xml绑定:

"@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>

当ImageView中同时使用了属性String imageUrl和Drawlable error,通过Binding adapter,这时的setter就是 loadImage()。

注意:数据绑定库忽略了用于匹配目的的自定义命名空间。

如果imageUrl和error都用于ImageView对象,imageUrl是字符串,而error是Drawable,则适配器会被调用。如果希望在设置任何属性时调用适配器,可以将adapter的可选requireAll标志设置为false,如下例所示:
KOTLIN

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String, placeHolder: Drawable) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}

JAVA

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}

注意:当存在冲突时,绑定绑定器将重写默认的数据绑定适配器。

绑定适配器方法可以选择在它们的处理程序中使用旧值。采用旧值和新值的方法应该首先声明属性的所有旧值,然后声明新值,如下面的示例所示:

KOTLIN

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(padding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

JAVA

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

事件处理者有可能只用于接口或抽象类中的一个抽象方法,如下面的示例所示:

KOTLIN

@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
        view: View,
        oldValue: View.OnLayoutChangeListener?,
        newValue: View.OnLayoutChangeListener?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue)
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue)
        }
    }
}

JAVA

@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 android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

当listener有多个方法时,必须将其分割成多个listener中。例如,View.OnAttachStateChangeListener有两种方法:onViewAttachedToWindow(View)和onViewDetachedFromWindow(View)。库提供两个接口来区分它们的属性和handlers:

KOTLIN

// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
    fun onViewDetachedFromWindow(v: View)
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
    fun onViewAttachedToWindow(v: View)
}

JAVA

@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也会影响另一个listener,因此需要一个适合于该属性的适配器。您可以在注解中将requireAll设置为false,以指定并非每个属性都必须被分配绑定表达式,如下面的示例所示:

KOTLIN

@BindingAdapter(
        "android:onViewDetachedFromWindow",
        "android:onViewAttachedToWindow",
        requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
        val newListener: View.OnAttachStateChangeListener?
        newListener = if (detach == null && attach == null) {
            null
        } else {
            object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(v: View) {
                    attach?.onViewAttachedToWindow(v)
                }

                override fun onViewDetachedFromWindow(v: View) {
                    detach?.onViewDetachedFromWindow(v)
                }
            }
        }

        val oldListener: View.OnAttachStateChangeListener? =
                ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener)
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener)
        }
    }
}

JAVA

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        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);
                    }
                }
            };
        }

        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

上面的示例比通常稍微复杂一些,因为View类使用addOnAttachStateChangeListener()和removeOnAttachStateChangeListener()方法而不是使用setter方法设置OnAttachStateChangeListener。android.databinding.adapters.ListenerUtil类帮助跟踪以前的listener,以便在binding adapter中删除它们。

通过使用@TargetApi(VER._CODES.HONEYCOMB_MR1)对接口OnViewDetachedFromWindow和OnViewAttachedToWindow进行注解,数据绑定代码生成器知道只有在在Android 3.1(API级别12)及更高版本才生成listener。

6.2 转换器

6.2.1 自动对象转换

当从一个绑定表达式返回一个对象,就会有一个setter被采用。该对象将会被转换成setter中的参数类型。
下面的例子,通过ObservableMaps来保存数据:

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

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

6.2.2 自定义转换

有时特定类型之间会自动转换。例如,设置背景:

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

这里,background的setter的参数类型应该是一个drawable,但color是一个整数。应当有一个转换规则,将int color转换为ColorDrawable。这种转换是通过使用一个@BindingConversion的静态方法来实现的:

KOTLIN

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

JAVA

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

注意,转换只发生在setter时期。所以不允许混合类型,如:

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

7. 绑定布局视图到架构组件

AndroidX库包括Architecture Components,您可以使用这些组件来设计健壮、可测试和可维护的应用程序。数据绑定库与体系结构组件无缝协作,以进一步简化UI的开发。应用程序中的布局可以绑定到体系结构组件中的数据,这些布局已经帮助您管理UI控制器的lifecycle并通知数据的更改。

这个页面显示了如何将架构组件合并到应用程序中,以进一步增强使用数据绑定库的好处。

Use LiveData to notify the UI about data changes

7.1 使用LiveData通知UI数据的变化

您可以使用LiveData对象作为数据绑定源来自动通知UI关于数据的更改。有关此架构组件的更多信息,请参见LiveData概述。

与实现Observable对象(如Observable字段)不同,LiveData对象知道订阅数据更改的观察者的生命周期。这种做法可以带来很多好处,这在使用LiveData的优点中得到了解释。在Android Studio版本3.1和更高版本中,可以用数据绑定代码中的LiveData对象替换observable字段。

为了在绑定类中使用LiveData对象,需要指定生命周期所有者来定义LiveData对象的范围。下面的示例指定了绑定类实例化后的生命周期所有者的活动:

KOTLIN

class ViewModelActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // Inflate view and obtain an instance of the binding class.
        val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)

        // Specify the current activity as the lifecycle owner.
        binding.setLifecycleOwner(this)
    }
}

JAVA

class ViewModelActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Inflate view and obtain an instance of the binding class.
        UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

        // Specify the current activity as the lifecycle owner.
        binding.setLifecycleOwner(this);
    }
}

可以使用ViewModel组件,正如在使用ViewModel管理UI相关数据所解释的,以将数据绑定到布局。在ViewModel组件中,可以使用LiveData对象来转换数据或合并多个数据源。下面的示例演示如何在ViewModel中转换数据:

KOTLIN

class ScheduleViewModel : ViewModel() {
    val userName: LiveData

    init {
        val result = Repository.userName
        userName = Transformations.map(result) { result -> result.value }
    }
}

JAVA

class ScheduleViewModel extends ViewModel {
    LiveData username;

    public ScheduleViewModel() {
        String result = Repository.userName;
        userName = Transformations.map(result, result -> result.value);
}

7.2 使用ViewModel管理UI相关的数据

Data Binding Library与ViewModel组件无缝地协作,ViewModel组件用于导出数据,这些数据被布局所观察并对其变化作出响应。使用带有Data Binding 库的ViewModel组件允许将UI逻辑从布局中移出并移入更容易测试的组件中。Data Binding 库确保View在需要时绑定和解绑数据源。剩下的大部分工作都是为了确保你暴露的是正确的数据。有关此架构组件的详细信息,请参阅ViewModel概述。

若要将ViewModel组件与Data Binding库一起使用,必须实例化从Viewmodel类继承的组件,获取绑定类的实例,并将ViewModel组件分配给绑定类中的属性。下面的示例演示如何使用库中的组件:

KOTLIN

class ViewModelActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // Obtain the ViewModel component.
        UserModel userModel = ViewModelProviders.of(getActivity())
                                                  .get(UserModel.class)

        // Inflate view and obtain an instance of the binding class.
        val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)

        // Assign the component to a property in the binding class.
        binding.viewmodel = userModel
    }
}

JAVA

class ViewModelActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Obtain the ViewModel component.
        UserModel userModel = ViewModelProviders.of(getActivity())
                                                  .get(UserModel.class);

        // Inflate view and obtain an instance of the binding class.
        UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

        // Assign the component to a property in the binding class.
        binding.viewmodel = userModel;
    }
}

在布局中,使用绑定表达式将ViewModel组件的属性和方法分配给相应的视图,如下面的示例所示:

"@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />

Use an Observable ViewModel for more control over binding adapters

7.3 使用Observable的ViewModel对binding adapters进行更多的控制

您可以使用实现Observable的ViewModel组件来通知其他的app组件有关数据的更改,这与使用LiveData对象类似。

在某些情况下,即使失去了LiveData的lifecycle管理能力,您可能更喜欢使用实现Observable接口的ViewModel组件。使用实现了Observable的ViewModel组件可以让您对app中的绑定适配器进行更多的控制。例如,当数据更改时,此模式为您提供了对notifications的更多控制,还允许您指定自定义方法来在双向数据绑定中设置属性的值。

要实现observable的ViewModel组件,必须创建从ViewModel类继承并实现Observable接口的类。当观察者使用addOnPropertyChangedCallback()和removeOnPropertyChangedCallback()方法订阅或取消订阅通知时,可以提供自定义逻辑。您还可以提供在notifyPropertyChanged()方法中属性更改时的自定义逻辑。下面的代码示例演示如何实现observable的ViewModel:

KOTLIN

/**
 * A ViewModel that is also an Observable,
 * to be used with the Data Binding Library.
 */
open class ObservableViewModel : ViewModel(), Observable {
    private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()

    override fun addOnPropertyChangedCallback(
            callback: Observable.OnPropertyChangedCallback) {
        callbacks.add(callback)
    }

    override fun removeOnPropertyChangedCallback(
            callback: Observable.OnPropertyChangedCallback) {
        callbacks.remove(callback)
    }

    /**
     * Notifies observers that all properties of this instance have changed.
     */
    fun notifyChange() {
        callbacks.notifyCallbacks(this, 0, null)
    }

    /**
     * Notifies observers that a specific property has changed. The getter for the
     * property that changes should be marked with the @Bindable annotation to
     * generate a field in the BR class to be used as the fieldId parameter.
     *
     * @param fieldId The generated BR id for the Bindable field.
     */
    fun notifyPropertyChanged(fieldId: Int) {
        callbacks.notifyCallbacks(this, fieldId, null)
    }
}

JAVA

/**
 * A ViewModel that is also an Observable,
 * to be used with the Data Binding Library.
 */
class ObservableViewModel extends ViewModel implements Observable {
    private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();

    @Override
    protected void addOnPropertyChangedCallback(
            Observable.OnPropertyChangedCallback callback) {
        callbacks.add(callback);
    }

    @Override
    protected void removeOnPropertyChangedCallback(
            Observable.OnPropertyChangedCallback callback) {
        callbacks.remove(callback);
    }

    /**
     * Notifies observers that all properties of this instance have changed.
     */
    void notifyChange() {
        callbacks.notifyCallbacks(this, 0, null);
    }

    /**
     * Notifies observers that a specific property has changed. The getter for the
     * property that changes should be marked with the @Bindable annotation to
     * generate a field in the BR class to be used as the fieldId parameter.
     *
     * @param fieldId The generated BR id for the Bindable field.
     */
    void notifyPropertyChanged(int fieldId) {
        callbacks.notifyCallbacks(this, fieldId, null);
    }
}

8.双向数据绑定

使用单向数据绑定,可以在属性上设置一个值,并设置一个监听器,该监听器对该属性的更改作出反应:

"@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>

双向数据绑定为这个过程提供了一条捷径:

"@+id/rememberMeCheckBox"
    android:checked="@={viewmodel.rememberMe}"
/>

@={} 符号(它主要包括“=”号)接收属性的数据更改,并同时监听用户更新。

为了对后台数据的变化作出反应,您可以使你的布局变量实现Observable(通常是BaseObservable),并使用@Bindable注解,如下面的代码片段所示:

KOTLIN

class LoginViewModel : BaseObservable {
    // val data = ...

    @Bindable
    fun getRememberMe(): Boolean {
        return data.rememberMe
    }

    fun setRememberMe(value: Boolean) {
        // Avoids infinite loops.
        if (data.rememberMe != value) {
            data.rememberMe = value

            // React to the change.
            saveData()

            // Notify observers of a new value.
            notifyPropertyChanged(BR.remember_me)
        }
    }
}

JAVA

public class LoginViewModel extends BaseObservable {
    // private Model data = ...

    @Bindable
    public Boolean getRememberMe() {
        return data.rememberMe;
    }

    public void setRememberMe(Boolean value) {
        // Avoids infinite loops.
        if (data.rememberMe != value) {
            data.rememberMe = value;

            // React to the change.
            saveData();

            // Notify observers of a new value.
            notifyPropertyChanged(BR.remember_me);
        }
    }
}

因为可绑定属性的getter方法称为getRememberMe(),所以属性的相应setter方法自动使用名称setRememberMe()。

有关使用BaseObservable和@Bindable的更多信息,请参见使用observable数据对象。

8.1 使用自定义属性的双向数据绑定

平台为最常见的双向属性和变更侦听器提供了双向数据绑定实现,您可以将其用作应用程序的一部分。如果希望使用带有自定义属性的双向数据绑定,则需要使用@InverseBindingAdapter和@InverseBindingMethod注解。

例如,如果希望在名为MyView的自定义视图中的”time”属性上启用双向数据绑定,请完成以下步骤:

  1. 使用@BindingAdapter注解设置初始值的方法,并在值改变时更新:

KOTLIN

@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
    // Important to break potential infinite loops.
    if (view.time != newValue) {
        view.time = newValue
    }
}

JAVA

@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
    // Important to break potential infinite loops.
    if (view.time != newValue) {
        view.time = newValue;
    }
}
  1. 注解从视图(被@InverseBindingAdapter注解)读取值的方法:

KOTLIN

@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
    return view.getTime()
}

JAVA

@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
    return view.getTime();
}

此时,数据绑定知道当数据改变时做什么(它调用用@BindingAdapter注解的方法),以及当视图属性改变时调用什么(它调用InverseBindingListener)。但是,它不知道属性何时或如何变化。

为此,需要在view上设置侦听器。它可以是与您的自定义view关联的自定义侦听器,也可以是一般事件,例如焦点丢失或文本更改。向设置监听器更改属性的方法添加@BindingAdapter注解:

KOTLIN

@BindingAdapter("app:timeAttrChanged")
@JvmStatic fun setListeners(
        view: MyView,
        final InverseBindingListener attrChange
) {
    // Set a listener for click, focus, touch, etc.
}

JAVA

@BindingAdapter("app:timeAttrChanged")
public static void setListeners(
        MyView view, final InverseBindingListener attrChange) {
    // Set a listener for click, focus, touch, etc.
}

listener包含一个InverseBindingListener作为参数。使用InverseBindingListener告诉数据绑定系统属性已更改。然后,系统可以调用使用了@InverseBindingAdapter注释的方法,以此类推。

注意:每一个双向绑定都会生成一个合成事件属性。此属性与基属性具有相同的名称,但它包含后缀”AttrChanged”。合成事件属性允许库创建使用@BindingAdapter注释的方法,以将事件侦听器关联到适当的View实例上。

实际上,这个侦听器包括一些复杂逻辑,包括单向数据绑定的侦听器。例如,请参见文本属性更改的适配器,TextViewBindingAdapter。

8.2 转换器

如果绑定到View对象的变量需要在显示之前进行格式化、翻译或更改,则可以使用Converter对象。

例如,获取一个显示日期的EditText对象:

id="@+id/birth_date"
    android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>

viewmodel.birthDate 属性包含Long类型的值,因此需要使用转换器来格式化。

因为使用了双向表达式,所以还需要一个反向转换器来让库知道如何将用户提供的字符串转换回支持的数据类型,在本例中为Long。这个过程是通过将@InverseMethod注解添加到一个转换器中并让这个注解引用逆转换器来完成的。此配置的一个示例出现在下面的代码片段中:

KOTLIN

object Converter {
    @InverseMethod("stringToDate")
    fun dateToString(
        view: EditText, oldValue: Long,
        value: Long
    ): String {
        // Converts long to String.
    }

    fun stringToDate(
        view: EditText, oldValue: String,
        value: String
    ): Long {
        // Converts String to long.
    }
}

JAVA

public class Converter {
    @InverseMethod("stringToDate")
    public static String dateToString(EditText view, long oldValue,
            long value) {
        // Converts long to String.
    }

    public static long stringToDate(EditText view, String oldValue,
            String value) {
        // Converts String to long.
    }
}

8.3 双向数据绑定中的无限循环

注意,在使用双向数据绑定时不要引入无限循环。当用户更改属性时,调用使用@InverseBindingAdapter注解的方法,并将值分配给backing属性。反过来,调用使用@BindingAdapter注解的方法,这将触发对使用@InverseBindingAdapter注解的方法的另一次调用,依此类推。

由于这个原因,通过比较使用@BindingAdapter注释的方法中的新值和旧值来打破可能的无限循环非常重要。

8.4 双向属性

当您使用下表中的属性时,平台为双向数据绑定提供内置支持。有关平台如何提供此支持的详细信息,请参阅相应绑定适配器的实现:

Class Attribute(s) Binding adapter
AdapterView android:selectedItemPosition
android:selection
AdapterViewBindingAdapter
CalendarView android:date CalendarViewBindingAdapter
CompoundButton android:checked CompoundButtonBindingAdapter
DatePicker android:year
android:month
android:day
DatePickerBindingAdapter
NumberPicker android:value NumberPickerBindingAdapter
RadioButton android:checkedButton RadioGroupBindingAdapter
RatingBar android:rating RatingBarBindingAdapter
SeekBar android:progress SeekBarBindingAdapter
TabHost android:currentTab TabHostBindingAdapter
TextView android:text TextViewBindingAdapter
TimePicker android:hour
android:minute
TimePickerBindingAdapter

8.5 附加资源

github上的TwoWaySample展示了本页讨论的概念的端到端示例。

你可能感兴趣的:(Android开源框架)