Data Binding 数据绑定(二)

本文建立在有一定使用 DataBinding 经验的基础之上,若还不熟悉 DataBinding 的用法,请参考前一篇博客Data Binding 数据绑定(一)

在学习 DataBinding 的过程中,参考 Google 官方的 DataBinding 示例 Demo,自己写了一个 DataBindingPractice Demo,用于练手。整个工程采用 MVP 架构 + DataBinding,欢迎 star、fork 和沟通交流。

本文介绍了 DataBinding 一些稍微高级的用法,主要包括以下四部分内容:

  1. DataBinding 中的数据对象(Data Objects)
  2. DataBinding 中生成绑定类(Generated Binding)
  3. DataBinding 中的属性设置(Attribute Setters)
  4. DataBinding 中的转换器(Converters)

数据对象(Data Objects)

  1. 任何普通的 Java 对象(POJO)都可以被 DataBinding 所使用,但是改变 POJO 对象的属性值并不会更新 UI 界面的显示。DataBinding 真正强大之处在于,它可以让你的数据对象具有通知 UI 界面对象的属性已经发生改变的能力。

  2. 有三种不同的数据变化通知机制:

  • Observable objects
  • observable fields
  • observable collection
  1. 如果这其中的一种数据对象被绑定到 UI 界面上,当数据对象的属性值发生变化时,UI 界面会自动更新。

可观察对象(Observable Objects)

  1. 一个类如果实现了 Observable 接口,那么 DataBinding 则会将一个 listener 绑定到该类上,就可以监听该类对象中的属性的变化。Observable 接口具有添加和移除 listener 的机制,但是否通知则取决于开发者。
  2. 为了使开发更容易,DataBinding 提供了一个名为 BaseObservable 的基类,它用于实现 listener 注册机制。
  3. 实现 Observable 的类负责什么时候通知该类的属性发生了变化,只需要在类的 getter 方法上添加 Bindable 注解,并在 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);
   }
}

在编译期间,使用 Bindable 注解标记过的 getter 方法会在 BR class 文件中生成一个入口,BR class 文件是在 Module 的包下,BR.classR.class 的功能类似。

可观察属性(ObservableFields)

  1. 创建一个 Observable 类还是需要一些工作量的,如果开发者不想花费太多的时间和精力,或者没有太多的属性需要观察监听的话,那么可以使用 ObservableField,或者它的子类: ObservableBooleanObservableByteObservableCharObservableShortObservableIntObservableLongObservableFloatObservableDoubleObservableParcelable
  2. ObservableField 是包含 Observable Object 对象的单一字段。原始版本避免了在获取过程中做打包和解包的操作。在数据对象中使用 ObservableField,需要创建一个 public final 字段,如下所示:
private static class User {
   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();

可观察集合(Observable Collections)

  1. 一些应用会使用动态的结构持有数据,可观察容器类允许使用键值对的形式来存取数据。
  2. 当键值对中的键是应用型数据(比如:String)时,ObservableArrayMap 是非常有用的。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在布局文件中,也可以通过使用 String 类型的键来获取到相应的值。

<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"/>
  1. 当键值对中的键是 Integer 型的,ObservableArrayList 则是非常有用的。
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);

在布局文件中,也可以通过使用 Integer 类型的键来获取到相应的值。

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


## 生成绑定类(Generated Binding)
1. 生成的绑定类通过布局文件中的 Views 和布局文件中的变量联系起来。
2. 如之前所讨论的那样,绑定类的名称和所在的位置都是可以自定义的。
3. 生成的所有的绑定类都是 `ViewDataBinding` 的子类。

### 构建(Creating)
1. 绑定类在 View Inflate 之后立即被创建,以确保在布局中的表达式被绑定到视图之前,View 的层次结构不会被打乱。
2. 有几种方式绑定布局文件,最常用的是使用 Binding 类中的静态方法来绑定类。`inflate` 方法调用一次就可以 Inflate View 并将 View 绑定到 Binding 类上。
3. 还有一个更加简单的方法,只需要一个 `LayoutInflater` 对象和一个 `viewGroup` 对象。
``` Java
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
  1. 如果布局使用另外不同的机制来 inflate,则可以单独绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
  1. 有时候,Binding 类的名字不得而知,在这种情况下,则可以使用 DataBindingUtil 生成该 Binding 类:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

带 ID 的 View(Views with IDs)

  1. 使用 DataBinding 库的布局文件,其中的每个带 ID 的 View,编译以后,都会在该布局文件对应的 Binding 类中生成一个被 public final 修饰的属性,Data Binding 会做一个简单的赋值,在 Binding 类中保存对应 ID 的 View。
  2. 通过这种机制获取控件比通过 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;

如果使用 DataBinding 库的话,在布局文件中为控件设置 Id 不是必须的,但是在某些情况下,在代码中通过 Id 得到控件还是有必要的。

变量(Variables)

布局文件中的每个变量都会生成对应的存取方法,如:

<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 类中,都会生成对应的存取方法,如下所示:

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

ViewStubs

  1. ViewStub 和普通的 View 相比是不一样的。它们最开始是不可见的,当它们被设置为可见的或者调用 inflate 方法时,ViewStub 会被替换为另外一个控件或布局。
  2. 因为最开始的时候,ViewStub 在布局层级中不可见,Binding 类中对应的控件也应该被移除,以便回收。
  3. 因为在 Binding 类中,所有 View 对应的属性都是被 final 字段修饰的,所以一个 ViewStubProxy 对象代替该 ViewStub,当 ViewStub 被设置为可见的或调用 inflate 方法之后,开发者可以通过此代理类 ViewStubProxy 得到对应的 ViewStub。
  4. inflate 一个新的布局时,必须为新的布局创建新的 Binding 类。所以 ViewStubProxy 必须监听 ViewStub 的 ViewStub.OnInflateListener,当 ViewStub 被 inflate 的时候,则建立一个新的 Binding 类。
  5. 因为 ViewStub 只能设置一个 OnInflateListener,开发者可以为 ViewStubProxy 设置一个 OnInflateListener,在 Binding 类被建立以后,OnInflateListener 就会被触发。
    代码如下所示:
<layout>
  ...
  <ViewStub
      android:id="@+id/viewStub"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout="@layout/layout_view_stub"/>
  ...
</layout>
mBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub stub, View inflated) {
                LayoutViewStubBinding mStubBinding = DataBindingUtil.findBinding(inflated);
                mStubBinding.tvViewStub.setOnClickListener((view1 -> showClickToast()));
            }
          });

高级绑定(Advanced Binding)

动态变量(Dynamic Variables)

  1. 有时候,一些 Binding 类不为人所知。比如,在 RecyclerView.Adapter 中可以用来处理不同的布局,此时便不知道该 Binding 类具体是什么类型的。而在 onBindViewHolder(VH, int) 方法中,ViewHolder 中的 Binding 类又必须被赋值。
  2. 在这个例子中,所有 RecyclerView 涉及到的布局中,都有一个 item 的变量。
  3. Adapter 所使用的 ViewHolder 中有一个 getBinding 的方法得到一个 ViewDataBinding 的 Binding 类。如下所示:
public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

立即绑定(Immediate Binding)

当变量或者 observable 变量发生变化时,会在下一帧才触发 Binding,但是有时候需要立即 Binding,可以通过 executePendingBindings() 方法立即触发 Binding。

后台线程

只要不是集合类型的数据,你可以在后台线程中更改数据。Data Binding 会在计算时将每个变量/字段在各个线程中做一份数据拷贝,以避免同步问题。

属性设置(Attribute Setters)

当一个属性值发生变化时,生成的 Binding 类必须调用该控件对应 data binding 表达式的 setter 方法。Data Binding 框架允许自定义调用何种方法改变值。

自动设置属性(Automatic Setters)

  1. 对于一个属性 attribute,Data Binding 会尝试着去找 setAttribute 方法。属性的命名空间是什么并没有什么关系,只和属性本身的名称有关。例如,为 TextView 的属性 android:text 设置了一个 binding 表达式,则 Data Binding 库会去寻找 setText(String) 的方法。
  2. 如果 data binding 表达式返回了一个 int 型数据,Data Binding 则会去寻找 setText(int) 的方法。对于 data binding 表达式的返回值一定要小心处理,如果必要的话,需要做类型强制装换。
  3. 需要注意的是,就算给定名称的属性不存在,Data Binding也会生效。正是因为如此,使用 Data Binding 则可以方便地自定义属性。例如,DrawerLayout 控件并没有什么属性,但是却有很多的 setters 方法,就可以方便地使用自动设置属性给 DrawerLayout 设置属性。
<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

重命名属性设置(Renamed Setters)

有些属性有其对应的 setter 方法,但是该 setter 方法和其属性名称并不是那么相匹配。对于这些方法,可以使用 BindingMethods 注解将该属性与对应的方法关联起来。例如:属性 android:tint 真正是和 setImageTintList(ColorStateList) 关联起来的,而不是和 setTint 方法关联:

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

在 Android 框架中实现的属性的 setter 方法已经不错,所以不需要开发者重命名属性设置了。

自定义属性设置(Custom Setters)

  1. 一些属性需要自定义逻辑。例如,没有一个 setter 方法和属性 android:paddingLeft 相关联,但是却存在 setPadding(left, top, right, bottom) 方法。被 BindingAdapter 注解修饰的静态 binding adapter 方法允许开发者自定义一个属性的 setter 方法如何被调用。
    Android 已经内置了一些 BindingAdapters。如下是一个与属性 paddingLeft 相关联的 setter 方法。
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}
  1. Binding adapters 在其他自定义类型上也非常好用。
    当开发者自定义的 binding adapters 与默认的 adapters 冲突时,开发者自定义的会覆盖默认的。
    当然也可以自定义接收多个参数的 adapters,一个在非主线程中加载图片的 Loader 如下所示:
@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}"/>
  ```
如果在一个 ImageView 中 `imageUrl` 属性和 `error` 属性同时被使用,并且 `imageUrl` 是 String 类型的,`error` 属性是 Drawable 类型的,则这个 `adapter` 将会被调用。
  * 在匹配的过程中,自定义的命名空间将会被忽略
  * 也可以为 android 命名空间编写 adapter

3. `binding adapter` 中的方法可以获取旧值,只需要将旧值放置在前,而新值放置在后,如下所示:
``` 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());
 }
}
  1. 事件处理 handlers 中,只可用于只拥有一个抽象方法的接口或抽象类,如下所示:
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue);
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue);
        }
    }
}
  1. 当一个 listener 中有多个方法时,它必须拆分成多个 listener。例如:View.OnAttachStateChangeListener 有两个方法:onViewAttachedToWindow()onViewDetachedFromWindow()。则必须为这两个方法设置不同的属性,分别处理其响应事件。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
    void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
    void onViewAttachedToWindow(View v);
}

因为改变一个 listener 必将会影响到另一个,所以我们必须有三个不同binding adapters,包括修改一个属性和修改两个属性的,如下所示:

@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,而不是通过 setter 方法设置监听器的。android.databinding.adapters.ListenerUtil 可以用来跟踪之前的 listener,并可以在 Binding Adaper 中移除监听器 listener
通过向 OnViewDetachedFromWindowOnViewAttachedToWindow 接口添加 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 注解,Data Binding 代码生成器知道监听器只在 Honeycomb MR1 设备或更新版本的设备中使用。

转换器(Converters)

对象转换(Object Conversions)

当 binding 表达式返回一个对象时,一个 setter 方法(自动 Setter,重命名 Setter,自定义 Setter),并将返回的对象强制转换成所选择的 setter 方法所需要的类型。
以下是一个使用 ObservableMaps 持有数据并转换的例子:

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

userMap 返回一个对象,并且这个对象会被自动地转换为 setter setText(CharSequence) 所需要的类型。当参数类型选择存在疑惑时,需要开发者手动地将数据类型进行转换。

自定义类型转换器(Custom Conversions)

有时候,属性的值需要在特定类型之间自动转换。例如,在设置背景的时候:

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

在这里,背景需要 Drawable 类型的,但是颜色却是 Integer 类型的。当需要一个 Drawable,binding 表达式返回的却是 Integer 的,所以此 int 型数据应该转换成 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"/>

DataBinding 第二篇文章也介绍完成。至此,关于 DataBinding 的文章到此就告一段落。如果有什么问题欢迎指出。我的工作邮箱:jiankunli24@gmail.com


参考资料:

DataBInding 官方文档

深入Android Data Binding(一):使用详解 -- YamLee

Android Data Binding 系列(一) -- 详细介绍与使用 -- ConnorLin

DataBinding(一)-初识 -- sakasa

(译)Data Binding 指南 -- 杨辉

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

推荐阅读更多精彩内容