Android官方数据绑定框架DataBinding

一、Data Binding是什么?

2015年的Google IO大会上,Android 团队发布了一个数据绑定框架(Data Binding Library),Data Binding实现了 MVVM 模型中的ViewModel。以后可以直接在 layout 布局 xml 文件中绑定数据了,无需再 findViewById 然后手工设置数据了。其语法和使用方式和 JSP 中的 EL 表达式非常类似。

Data Binding Library 是一个 support 库,支持 Android 2.1+ 版本 (API level 7+)。 由于该框架需要使用编译器来生成很多代码,所以需要配合新版本的 Android Studio (1.3.0-beta1 + 版本)才能使用,Gradle 插件1.5.0-alpha1以上。

二、配置环境

  • Android Studio版本是1.3+,更新Support repository到最新的版本。
    新建一个project,在dependencies中添加以下依赖

    classpath "com.android.databinding:dataBinder:1.0-rc1"
  • 新建module,并且在module的build.gradle文件中添加

    apply plugin: 'com.android.application'
    apply plugin: 'com.android.databinding'
  • Studio1.5+忽略以上两点,只需在对应module的build.gradle中添加

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

    ps:即使依赖的库中使用了data binding,该module也必须在build.gradle中配置

三、初体验

3.1 布局文件

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

3.2 数据对象

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

3.3 绑定数据

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

在 ListView 或者 RecyclerView 的Adapter中使用:

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

3.4 绑定事件

如果你的数据对象中有两个方法:

public class MyHandlers {
    public void onClickFriend(View view) { ... }
    public void onClickEnemy(View view) { ... }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.Handlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
           android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
   </LinearLayout>
</layout>

四、详细说明

4.1 导包

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

像java一样,java.lang.* 自动导入,可以直接使用

当存在同名的类时,其中一个可以使用别名

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

导入类还可以用于在表达式中引用静态属性和方法

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

4.2 变量

自动生成的binding类会为每个声明的变量生成setter和getter方法,在调用setter方法之前变量的值为java中对应类型的默认值。
当表达式中需要使用时,会自动生成一个名为“context”的变量,“context”的值是由根布局的getContext()获得,如果手动声明了一个名为“context”的变量,默认的会被覆盖。

4.3 自定义Binding类名

默认Binding类会根据布局文件名自动生成,放在module包下的databinding包中,如布局文件contact_item.xml会自动生成ContactItemBinding,如果module包名是com.example.my.app,这个类将被放在com.example.my.app.databinding下。通过class属性,可以修改Binding类的类名和所在包。

  • databinding包下,类名为ContactItem

    <data class="ContactItem">
        ...
    </data>
  • module包下

    <data class=".ContactItem">
        ...
    </data>
  • 指定包名

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

4.4 Includes

使用include时,需要将变量传递到被包含的布局中,下面例子中 name.xmlcontact.xml 中必须有user变量。

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

Data binding不支持<merge>下直接的子元素使用include,比如下面的就不支持:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <merge>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

五、表达式

5.1 普通

以下表达式和java的一样
* 算术表达式 + - / * %
* 字符串连接 +
* 逻辑运算符 && ||
* 位运算符 & | ^
* 一元运算符 + - ! ~
* 位移运算符 >> >>> <<
* 关系运算符 == > < >= <=
* instanceof
* 分组 ()
* 字面值 - 字符, 字符串, 数字, null
* 类型转换
* 方法调用
* 访问成员变量
* 访问数组 []
* 三元运算符 ?:

例:

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

5.2 缺少的

java中可以使用而这里不能使用的
* this
* super
* new
* Explicit generic invocation

5.3 判断非空运算符

当表达式左边不为null时使用左边的值,如果为null使用右边的

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

等效于

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

5.4 避免了NullPointerException

生成的data binding代码会自动检查空值,避免空指针。例如表达式@{user.name},如果usernulluser.name将使用默认值nulluser.age将使用默认值0。

5.5 集合(Collections

集合使用泛型时小于号<需要用&lt;代替

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String>"/>
    <variable name="sparse" type="SparseArray&lt;String>"/>
    <variable name="map" type="Map&lt;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`}" <!-- 中间是反单引号(数字键1左边) -->
android:text="@{map[&quot;firstName&quot;]}"

5.6 资源文件(Resources

  1. 直接在表达式中使用resources

    android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
  2. 格式化带参数的字符串

    android:text="@{@string/name(name)}"
    android:text="@{@string/name_and_gender(name, gender)}"
    <!-- s是字符串,d是整数 -->
    <string name="name">姓名:%s</string>
    <string name="name_and_gender">姓名:%1$s;性别:%2$s</string>
  3. 格式化带参数的plural,使用较少,这里不做过多解释

    Have an orange
    Have %d oranges
    
    android:text="@{@plurals/orange(orangeCount, orangeCount)}"
  4. 有些resource必须明确指出类型

    类型普通引用表达式引用
    String[]@array@stringArray
    int[]@array@intArray
    TypedArray@array@typedArray
    Animator@animator@animator
    StateListAnimator@animator@stateListAnimator
    color int@color@color
    ColorStateList@color@colorStateList

    sample

    android:text="@{@stringArray/genders[1]}"
    android:text="@{String.valueOf(@intArray/ages[2])}"
    <array name="genders">
        <item></item>
        <item></item>
    </array>
    <array name="ages">
        <item>11</item>
        <item>12</item>
        <item>13</item>
    </array>

六、数据对象

任何一个简单的Java对象(POJO)都可以用来绑定,但是修改一个POJO不能够触发UI更新。
当Observable objects, observable fields, 和 observable collections 这3种数据对象绑定到UI,并且数据改变时UI会自动更新

6.1 Observable Objects

实现 android.databinding.Observable 接口。为了方便,Android提供了基类BaseObservable,实现了监听器的注册机制。

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 中生成一个entry。 BR是在编译时生成在module的一个类,功能与R.java类似。

6.2 ObservableFields

属性较少时可以使用ObservableField,包含ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和 ObservableParcelable,简单的POJO就可以实现。

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

6.3 Observable Collections

6.3.1 ObservableArrayMap(当key是引用类型时)

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&lt;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"/>

6.3.2 ObservableArrayList(当key是整数时)

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&lt;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中的View,就如前面说的,Binding的类名和包名都可以定制,生成的binding类都继承自ViewDataBinding

7.1 创建

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

If the layout was inflated using a different mechanism, it may be bound separately:

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

7.2 带ID的View

如果布局中某个View设置了id,则生成的binding类中会包含:

public final TextView firstName;
public final TextView lastName;

使用时直接调用 binding.firstName

7.3 ViewStubs

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
        ...>
        <ViewStub
            android:id="@+id/view_stub"
            android:layout="@layout/view_stub"
            ... />
    </LinearLayout>
</layout>
binding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
    @Override
    public void onInflate(ViewStub stub, View inflated) {
        ViewStubBinding binding = DataBindingUtil.bind(inflated);
        User user = new User("fee", "lang");
        binding.setUser(user);
    }
});

7.4 高级用法

7.4.1 动态变量

以 RecyclerView 为例,Adapter 的 DataBinding 需要动态生成,因此我们可以在 onCreateViewHolder 的时候创建这个 DataBinding,然后在 onBindViewHolder 中获取这个 DataBinding。

@Override
public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    ViewDataBinding binding = DataBindingUtil.bind(LayoutInflater.from(parent.getContext())
            .inflate(layoutId, parent, false));
    BindingHolder bindingHolder = new BindingHolder(binding.getRoot());
    bindingHolder.setBinding(binding);
    return bindingHolder;
}

@Override
public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   // 当一个变量改变时,binding会在下一帧时改变UI,需要立刻执行,可以使用executePendingBindings()方法。
   holder.getBinding().executePendingBindings();
}

public static class BindingHolder extends RecyclerView.ViewHolder {
    private ViewDataBinding binding;

    public BindingHolder(View itemView) {
        super(itemView);
    }

    public ViewDataBinding getBinding() {
        return binding;
    }

    public void setBinding(ViewDataBinding binding) {
        this.binding = binding;
    }
}

八、Attribute Setters

当一个绑定的值改变时,可以指定调用哪个方法来设置值

8.1 Automatic Setters

没有配置自定义属性,data binding会自动找到对应的setAttribute方法(命名空间无所谓),注意表达式的返回值,必要的时候进行类型转换。

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

8.2 Renamed Setters

如果不想调用根据名字匹配的setter,可以通过BindingMethods注解重新匹配。

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

通常开发者不需要重命名setters

8.3 Custom Setters

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

还可以接收多个参数

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

这个适配器会在imageUrl和error都在ImageView中设置,imageUrl是字符串,error时drawable时调用

Binding adapter methods may optionally take the old values in their handlers. A method taking old and new values should have all old values for the attributes come first, followed by the new values:

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

当一个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经常会影响另一个,所以我们需要有3个不同的绑定适配器。

@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来操作listener,不使用set方法来操作View.OnAttachStateChangeListener。The android.databinding.adapters.ListenerUtil class helps keep track of the previous listeners so that they may be removed in the Binding Adaper.

By annotating the interfaces OnViewDetachedFromWindow and OnViewAttachedToWindow with @TargetApi(VERSION_CODES.HONEYCOMB_MR1), the data binding code generator knows that the listener should only be generated when running on Honeycomb MR1 and new devices, the same version supported by addOnAttachStateChangeListener(View.OnAttachStateChangeListener).

九、转换器(Converters)

9.1 Object Conversions

当binding表达式返回一个对象时,会从automatic, renamed, 和 custom setters 中选择一个,这个对象会被转换成选择的setter的参数类型。
这方便了用ObservableMaps保存数据,例如:

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

userMap 返回了一个 Object ,这个 Object 将会自动转换成 setText(CharSequence)的参数类型。当参数类型不明确时,开发者需要在表达式中进行转换。

9.2 Custom Conversions

有时特殊的类型需要自动转换。比如,当设置背景时:

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

这里,背景是一个 Drawable,但是颜色是一个整数,需要把 int 转换成 ColorDrawable,可以使用带有 BindingConversion 注解的一个静态方法:

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

Note:不要将上面的和以下混淆

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值