转载

Android Architecture Components 系列二(DataBinding)

按照官方的解释,数据绑定库是一个支持库,允许您使用声明性格式而不是以编程方式将布局中的UI组件绑定到应用程序中的数据源。听起来会比较的抽象,具体举个例子来说就比较好理解一些呢。下面这段代码是我们经常可以看到的一段代码:

TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());
复制代码

这就是官方所说的以编程方式将布局中的UI组件绑定到应用程序中,而使用数据绑定的方式时则是使用如下的形式:

<TextView
    android:text="@{viewmodel.userName}" />
复制代码

这样便可以节省很多在activity中的数据绑定的代码,使其更简单更易于维护,还可以提高应用程序的性能,并有助于防止内存泄漏和空指针异常。这样的事情何乐而不为呢,因此我们还是非常有必要对其进行学习的。

2.环境的配置

DataBinding支持运行在Android 4.0(API级别14)或更高版本的设备中,gradle版本支持1.5.0及其上的版本。要将应用程序配置为使用数据绑定,在app模块的build.gradle中将该一下代码添加到文件中:

android {
    ...
    dataBinding {
        enabled = true
    }
}
复制代码

3.布局与绑定表达式

DataBinding自动生成将布局中的视图与数据对象绑定所需的类,下面就用一个官方的例子进行解释,个人觉得也更好理解一些:

<?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>
复制代码

上面这就是一个DataBinding库在布局中的基本的形式了,可以看到最顶层的根元素是一个layout,这也是DataBinding库布局文件的基础,然后data主要是存放一些数据的类,如上面这个例子,我们写一个User的实体类,然后在data元素中进行定义,这样我们就可以在布局文件中使用user这个对象,这样就能将很多我们在代码中的事情可以直接在布局中进行配置了,如上布局中的表达式使用“@{}”语法写入属性属性中。这里,TextView文本设置为变量的 user的firstName属性。

3.1 数据对象

User其实就是我们经常在代码中所写的实体类这样的,如下:

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

复制代码

假设上面的布局文件名称为activity_main.xml,数据绑定库就会为我们自动生成一个叫做ActivityMainBinding的类,这个类包含布局属性(例如,user变量)到布局视图的所有绑定,并知道如何为绑定表达式赋值。如果我们要在代码中设置User的属性,则可以使用如下的代码:

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

复制代码

同时你也可以通过使用一个LayoutInflater来获得数据绑定是对象,如下:

ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
复制代码

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

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

3.2 操作符

数据绑定库允许在布局文件中使用操作符,具体的一些操作符借用官方列举的如下图所示:

Android Architecture Components 系列二(DataBinding)

下面一个实例,相信大家看了都会非常好理解了:

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

在数据绑定库中有一个比较特殊的运算符,就是“??”,官方对其命名为Null coalescing operator,我们这里把它译做空结合运算符吧,还是通过一个简单的例子来理解这个运算符:

android:text="@{user.displayName ?? user.lastName}"
复制代码

上面这一段所代表的意思就是如果user.displayName为null那么就使用user.lastName这个数据作为这里的数据,不为null就使用user.displayName,这个跟我们在intent间传递数据挺像的,我们也是可以设定一个当获得的对象为空时使用一个默认值。这样可以很好的避免空指针异常的发生。

3.3 集合

数据绑定库中同样支持数组,list以及map,具体可以看如下的例子:

<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]}"
复制代码

map的使用中,如果你要获取某个key所对应的值,可以像如下两种形式:

android:text='@{map["firstName"]}'
//or
android:text="@{map[`firstName`]}"
复制代码

3.4 资源

您可以使用以下语法访问表达式中的资源:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
复制代码

3.5 事件处理

数据绑定同样可以使用控件的一些属性(例如,onClick()方法),如下:

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.MyHandlers"/>
       <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>
复制代码

上面的例子就给我们很好的展示了在DataBinding中如何使用相关的属性。

3.6 导入、变量和包含

数据绑定库提供诸如导入,变量和包含之类的功能。导入使布局文件中的类很容易引用。变量允许您描述可用于绑定表达式的属性。包括让您在整个应用中重复使用复杂的布局。

3.6.1 导入与变量

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

<data>
    <import type="android.view.View"/>
</data>
复制代码

当存在类名冲突时,可以将其中一个类重命名为别名。以下示例将包中的View类 重命名com.example.real.estate为Vista:

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

同样也可以导入其他的类,导入的类型可以用作变量和表达式中的类型引用。以下示例显示User并List用作变量的类型:

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

关于变量的定义,上面variable中便是在进行变量的定义。

3.6.2 包括

通过使用app命名空间和属性中的变量名,变量可以从包含的布局传递到包含的布局绑定中。以下示例显示user了name.xml和 contact.xml布局文件中包含的变量:

<?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>
复制代码

不过数据绑定不支持include作为merge元素的直接子元素。例如,不支持以下布局:

<?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><!-- Doesn't work -->
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>
复制代码

4.使用可观察的数据对象

可观察性是指对象通知其他人数据变化的能力。数据绑定库允许您使对象,字段或集合可观察。这里其实就相当于我们可以在数据绑定库中使用RxJava的可观察对象,当其中一个可观察数据对象绑定到UI并且数据对象的属性发生更改时,UI将自动更新。

4.1 Observable fields

一些工作涉及创建实现Observable接口的类,如果您的类只有一些属性,那么这些工作是 不值得的。在这种情况下,您可以使用泛型 Observable类和以下特定于原语的类来使字段可观察:

Android Architecture Components 系列二(DataBinding)

使用的例子:

private static class User {
    public final ObservableField<String> firstName = new ObservableField<>();
    public final ObservableField<String> lastName = new ObservableField<>();
    public final ObservableInt age = new ObservableInt();
}
复制代码
user.firstName.set("Google");
int age = user.age.get();
复制代码

对于RxJava有过一些了解的小伙伴相比对上面的这种表达应该是可以比较好理解的,如果不是很理解建议去了解一下RxJava,同样也是对我们会有非常大的帮助。

4.2 Observable collections

一些应用程序使用动态结构来保存数据。可观察集合允许使用密钥访问这些结构。在 ObservableArrayMap 当键是一个引用类型,类是有用的,例如String,如示于下面的例子:

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

在布局中,可以使用字符串键找到对应的值,如下所示:

<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);
复制代码

在布局中,可以通过索引访问列表,如以下示例所示:

<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"/>
复制代码

4.3 Observable objects

一个实现Observable接口的类允许希望被通知的可观察对象注册侦听器。

该Observable接口具有添加和删除侦听器的机制,但您必须决定何时发送通知。为了使开发更容易,数据绑定库提供了BaseObservable实现侦听器注册机制的类。实现的数据类BaseObservable负责通知属性何时更改。这是通过Bindable为getter分配注释并notifyPropertyChanged()在setter中调用方法来完成的,如以下示例所示:

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);
    }
}
复制代码

数据绑定库生成BR在模块包中命名的类,该类包含用于数据绑定的资源的ID。该Bindable注释产生的一个条目BR编译期间类文件。如果无法更改数据类的基类,则Observable可以使用PropertyChangeRegistry对象来实现接口,以便有效地注册和通知侦听器。

5.绑定适配器

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

数据绑定库允许您指定调用的方法来设置一个值,提供自己的绑定逻辑,并指定通过使用适配器返回的对象的类型。

5.1 设置属性值

只要绑定值发生变化,生成绑定类必须在视图上调用setter方法绑定表达式。你可以允许数据绑定库来自动确定方法,显式地声明方法,或提供自定义逻辑选择方法。

5.1.1 方法的自动选择

对于一个属性命名为example,库自动试图找到方法setExample(arg)接受兼容的类型作为参数。属性的名称空间不被考虑,只根据属性名称和类型搜索一个方法。

例如,android:text="@{user.name}"表达式,库查找setText(arg)方法,它接受user.getName()返回的数据类型。user.getName()的返回类型是字符串,库查找setText()方法,它接受一个字符串参数。相反如果表达式返回一个int,图书馆搜索setText()方法,该方法接受一个int参数。表达式必须返回正确的类型,如果必要的话你可以把返回值转换成你想要的类型。

即使没有给定名称的属性,数据绑定仍然有效。然后,您可以使用数据绑定为任何setter创建属性。例如,支持类DrawerLayout没有任何属性,但有很多setter。以下布局分别自动使用 setScrimColor(int)和setDrawerListener(DrawerListener)方法作为app:scrimColor和app:drawerListener属性的setter :

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

5.1.2 指定自定义方法名称

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

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

大多数情况下,您不需要在Android框架类中重命名setter。已使用名称约定实现的属性可自动查找匹配方法。

5.1.3 提供自定义逻辑

某些属性需要自定义绑定逻辑。例如,该android:paddingLeft属性没有关联的setter。相反,setPadding(left, top, right, bottom)提供了该方法。带BindingAdapter注释的静态绑定适配器方法允许您自定义如何调用属性的setter。

Android框架类的属性已经BindingAdapter创建了注释。例如,以下示例显示了paddingLeft属性的绑定适配器:

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

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

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

当发生冲突时,您定义的绑定适配器将覆盖Android框架提供的默认适配器。

您还可以使用接收多个属性的适配器,如以下示例所示:

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.get().load(url).error(error).into(view);
}
复制代码

您可以在布局中使用适配器,如以下示例所示。请注意,@drawable/venueError是指您应用中的资源。围绕资源@{}使其成为有效的绑定表达式。

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
复制代码

该适配器如果同时被imageUrl和error作用于同一个ImageView对象,imageUrl是一个字符串,error是一个Drawable。如果要在设置任何属性时调用适配器,可以将适配器的可选requireAll 标志设置 为false,如以下示例所示:

@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);
  }
}
复制代码

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

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

当侦听器具有多个方法时,必须将其拆分为多个侦听器。例如,View.OnAttachStateChangeListener有两种方法:onViewAttachedToWindow(View)和onViewDetachedFromWindow(View)。该库提供了两个接口来区分它们的属性和处理:

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}
复制代码

因为更改一个侦听器也会影响另一个侦听器,所以需要一个适用于任一属性或适用于两者的适配器。您可以在注释中设置 requireAll 为false指定不是必须为每个属性分配绑定表达式,如以下示例所示:

@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 类可以帮助跟踪以前的听众,让他们可以在绑定的适配器中删除。

通过注释的接口OnViewDetachedFromWindow和OnViewAttachedToWindow用@TargetApi(VERSION_CODES.HONEYCOMB_MR1),数据绑定代码发生器知道只能运行在Android 3.1(API级12)和更高,addOnAttachStateChangeListener()方法也是同样的版本支持。

5.2 对象转换

5.2.1 自动对象转换

当O绑定表达式返回一个object对象时,库会选择用于设置属性值的方法。这个Object 被转换为所选择的方法的参数类型。在应用程序中使用ObservableMap类存储数据,此行为很方便 ,如以下示例所示:

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

userMap表达式中的对象返回一个值,该值自动转换为在setText(CharSequence)用于设置android:text属性值的方法中找到的参数类型。如果参数类型不明确,则必须在表达式中强制转换返回类型。

5.2.2 自定义转化

在某些情况下,特定类型之间需要自定义转换。例如,android:background视图的属性需要一个Drawable,但color指定的值是整数。以下示例显示了一个期望属性是Drawable,但是提供了一个整形:

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

每当期望一个Drawable但是返回一个int时,int应该被转换为ColorDrawable。可以使用带BindingConversion注释的静态方法完成转换 ,如下所示:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}
复制代码

但是,绑定表达式中提供的值类型必须一致。你不能在同一表达式中使用不同的类型,如以下示例所示:

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

6.将布局视图绑定到体系结构组件

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

本章讲解如何将数据绑定库与架构组件在你的应用程序中完美结合使用。

6.1 使用LiveData通知UI有关数据更改的信息

你可以使用LiveData对象作为数据绑定源来自动通知UI有关数据更改的信息。LiveData的具体使用会在后面的章节进行讲述。

不同于实现对象Observable-例如 observable fields - LiveData对象了解订阅数据变化的观察者的生命周期。这点带来了许多好处,这些都在使用LiveData的优势中进行了解释。在Android Studio 3.1及更高版本中,您可以使用数据绑定代码中的对象替换 可观察字段LiveData。

要将LiveData对象与绑定类一起使用,需要指定生命周期所有者以定义LiveData对象的范围。以下示例在实例化绑定类之后将活动指定为生命周期所有者:

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:

class ScheduleViewModel extends ViewModel {
    LiveData username;

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

6.2 使用ViewModel管理与UI相关的数据

数据绑定库与ViewModel组件无缝协作, 组件公开布局观察到的数据并对其更改做出反应。通过使用 ViewModel数据绑定库中的组件,您可以将UI逻辑从布局移动到组件中,这些组件更易于测试。数据绑定库可确保在需要时绑定和取消绑定数据源。剩下的大部分工作都在于确保您公开正确的数据。

要将ViewModel组件与数据绑定库一起使用,必须实例化从ViewModel类中继承的组件, 获取绑定类的实例,并将ViewModel组件分配给绑定类中的属性。以下示例显示如何将该组件与库一起使用:

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使用绑定表达式将组件的属性和方法分配给相应的视图,如以下示例所示:

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />
复制代码

6.3 使用Observable ViewModel可以更好地控制绑定适配器

您可以使用一个实现Observable的ViewModel组件来通知其他应用程序组件有关数据更改的信息,类似于您使用LiveData对象的方式。

在某些情况下,您可能更喜欢使用ViewModel实现Observable接口而不是使用LiveData对象的 组件,即使您丢失了LiveData的生命周期管理功能。使用一个实现Observable的ViewModel组件可以更好地控制应用程序中的绑定适配器。例如,此模式使您可以在数据更改时更好地控制通知,还允许您指定自定义方法以在双向数据绑定中设置属性的值。

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

/**
 * 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);
    }
}
复制代码

7. 双向数据绑定

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

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>
复制代码

双向数据绑定提供了此过程的快捷方式:

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@={viewmodel.rememberMe}"
/>
复制代码

@={}符号,其中重要的包括符号“=”,接收数据属性的变化,并同时监听用户的更新。

为了对后台数据中的更改做出反应,您可以使你的布局变量实现Observable,通常 的是BaseObservable,并使用 @Bindable注释,如以下代码段所示:

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);
        }
    }
}
复制代码

由于调用了bindable属性的getter方法getRememberMe(),因此该属性的相应setter方法会自动使用该名称 setRememberMe()。

7.1 使用自定义属性进行双向数据绑定

该平台为最常见的双向属性和更改侦听器提供双向数据绑定实现,你可以将其用作应用程序的一部分。如果要对自定义属性使用双向数据绑定,则需要使用@InverseBindingAdapter和@InverseBindingMethod 进行注释。

例如,如果要"time"在调用的自定义视图中对属性启用双向数据绑定MyView,请完成以下步骤:

1.注释设置初始值的方法,并在值更改时使用@BindingAdapter以下内容进行更新:

@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
    // Important to break potential infinite loops.
    if (view.time != newValue) {
        view.time = newValue;
    }
}
复制代码

2.使用以下方法注释从视图中读取值的方法 @InverseBindingAdapter:

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

此时,数据绑定知道数据更改时要做什么(它调用带注释的方法 @BindingAdapter)以及视图属性更改时调用的内容(它调用 InverseBindingListener)。但是,它不知道属性何时或如何更改。

为此,您需要在视图上设置一个侦听器。它可以是与自定义视图关联的自定义侦听器,也可以是通用事件,例如失去焦点或文本更改。将@BindingAdapter注释添加到为该属性的更改设置侦听器的方法:

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

监听器包括InverseBindingListener作为参数。您可以使用它 InverseBindingListener来告诉数据绑定系统该属性已更改。然后,系统可以开始调用使用注释的方法 @InverseBindingAdapter,依此类推。

7.2 转换器

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

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

<EditText
    android:id="@+id/birth_date"
    android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>
复制代码

该viewmodel.birthDate属性包含type的值Long,因此需要使用转换器对其进行格式化。

由于正在使用双向表达式,因此在这种情况下,还需要使用逆转换器让库知道如何将用户提供的字符串转换回后备数据类型Long。此过程通过将@InverseMethod注释添加到其中一个转换器并使此注释引用逆转换器来完成。以下代码段中显示了此配置的示例:

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.
    }
}
复制代码

7.3 使用双向数据绑定的无限循环

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

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

7.4 双向属性

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

Android Architecture Components 系列二(DataBinding)
原文  https://juejin.im/post/5cc121355188252d8c521d15
正文到此结束
Loading...