Android Databinding使用

构建环境

要使用Data Binding,需要添加dataBinding元素到app模块下的build.gradle文件下。

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

创建数据绑定布局文件

数据绑定的布局文件和普通的布局文件对比有如下不同:根标签是layout,接下来是一个data元素和一个view的根元素。

<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <variable name="user" type="cn.malinkang.databinding.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>

variable元素用来定义变量,type是变量类型,name是变量名。

在布局中通过@{}语法将变量的属性值设置给View的属性。

数据绑定中的data可以是POJO对象,也可以是JavaBean对象。在POJO对象中@{}获取的值是直接调用属性的值,而JavaBean则是通过属性的get方法获取值。例如上面的例子中android:text="@{user.firstName},如果User是POJO对象,则访问的是firstName属性,如果是JavaBean,则是通过调用getFirstName()方法获取值。

POJO对象:

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

JavaBean对象:

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

导入

数据绑定的布局文件允许利用import元素像Java一样导入其他数据类型。例如下面代码就导入一个View。

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

现在,View对象可以在绑定表达式中使用。

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

当类名出现冲突,可以指定一个别名

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

除了可以在表达式中使用,也可以在variable中使用。

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

导入的类型还可以在表达式中使用static属性和方法

<data>
    <import type="com.example.MyStringUtils"/>
    <variable name="user" type="com.example.User"/>
</data>
…
<TextView
   android:text="@{MyStringUtils.capitalize(user.lastName)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

引入

如果相同的布局被重复使用,我们会把他单独放在一个布局文件中使用include标签引入。数据绑定可以使用应用命名空间和变量名将变量从容器布局中传递到被包含的布局中。

被包含的布局文件user.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <import type="cn.malinkang.databinding.User"/>
    <variable
        name="user"
        type="User"
        />
  </data>
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical"
      >
    <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>

容器布局文件activity_main.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="cn.malinkang.databinding.User"
        />
  </data>
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      >
    <include
        layout="@layout/user"
        bind:user="@{user}"
        />
    <include
        layout="@layout/user"
        bind:user="@{user}"
        />
  </LinearLayout>
</layout>

绑定数据

数据绑定是通过Binding类进行绑定的,创建一个数据绑定布局,编译器会自动生成一个Binding类。该类包含了设置变量的方法,比如上面的布局生成的绑定类会有一个setUser(User User)方法。

默认情况下,一个Binding类会基于布局文件的名称而产生,将布局文件名改为驼峰命名,并添加“Binding”后缀。例如布局文件为activity_main.xml,则会生的类名为ActivityMainBinding

在Activity中创建Binding类的实例可以通过DataBindingUtilsetContentView方法来创建。

ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("linkang","ma");
binding.setUser(user);

也可以通过生成的Binding类的inflate方法获取View,该方法只会生成View不会添加到Activity上。

ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
User user = new User("linkang","ma");
binding.setUser(user);
setContentView(binding.getRoot());

在Fragment中创建Binding类。

FragmentBinding binding =
        FragmentBinding.inflate(inflater, container, false);
//或者
FragmentBinding binding =
    DataBindingUtil.inflate(inflater, R.layout.fragment_my, container, false);
User user = new User("linkang", "ma");
binding.setUser(user);
return binding.getRoot();

上面已经了解了Binding类名的生成规则,我们也可以自定义生成的Binding类的类名。data元素的class属性可以指定生成的类名。

<data class="MainBinding">
...
</data>

生成的MainBinding类位于MainActivity包下的databinding包中,比如MainActivity位于cn.malinkang.databinding的包中,MainBinding位于cn.malinkang.databinding.databinding包中。还可以通过.前缀指定相对包名,例如在MainBinding前面添加.,生成的MainBinding类位于cn.malinkang.databinding包中。

<data class=".MainBinding">
...
</data>

也可以指定全包名

<data class="com.malinkang.MainBinding">
...
</data>

事件处理

数据绑定允许写表达式来处理views分发的事件,比如onClick事件。事件属性名称与监听器方法的名称一致,例如View.OnLongClickListener监听器方法为onLongClick(),对应属性为android:onLongClick

有两种处理事件的方法:

  • 方法引用
  • 监听器绑定

方法引用

当设置的方法的参数签名和监听器实现方法的签名一致时,则采用方法引用。数据绑定将方法引用和当前View对象一起传递给一个监听器,并将这个监听器设置给当前View对象。

public class MyHandlers {
  public static final String TAG = MyHandlers.class.getSimpleName();

  public void showLog(View view) {
    Log.d(TAG, "showLog: " + view);
  }
  public void showToast(View view,User user){
    Toast.makeText(view.getContext(), "hello,"+user.getFirstName(), Toast.LENGTH_SHORT).show();
  }
}

showLog(View view)签名与onClick(View view)方法签名一致,采用方法引用

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto"
    >
  <data>
    <variable
        name="handlers"
        type="cn.malinkang.databinding.MyHandlers"
        />

  </data>
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      >
    <Button
        android:text="Handle Event"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="@{handlers::showLog}"
        />
  </LinearLayout>
</layout>

监听器绑定

监听器绑定是当事件发生时运行的绑定表达式。它们与方法引用类似,但它们允许你运行任意数据绑定表达式。

在方法引用中,方法的参数必须与事件监听器的参数匹配。当方法签名与监听器方法签名不一致时,采用监听器绑定。上面的showToast方法与方法签名不一致,则采用监听器绑定。

<?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="handlers"
        type="cn.malinkang.databinding.MyHandlers"
        />

  </data>
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      >
    <Button
        android:text="Handle Event"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="@{(v) -> handlers.showToast(v,user)}"
        />
  </LinearLayout>
</layout>

表达语言

常见特性

表达式语言看起来很像Java表达式。下面这些表达式用法是一样的:

  • 数学运算符+ - * / %
  • 字符串连接符 +
  • 逻辑运算符 && ||
  • 位运算符 & | ^
  • 一元运算 + - ! ~
  • 移位 >> >>> <<
  • 比较运算符 == > < >= <=
  • instanceof
  • 分组()
  • null
  • Cast
  • 方法调用
  • 数组访问 []
  • 三元运算 ?:

不支持的操作

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

Null合并操作

??- 左边的对象如果它不是null,选择左边的对象;或者如果它是null,选择右边的对象。例如下面的例子中如果displayName不为空则将user.displayName设置给text属性,如果为空则将user.lastName设置给text属性。

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

上面表达式等价于下面的表达式

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

避免空指针

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

集合

常用的集合包括ArraysListSparseArrayMap,这些都可以使用[]操作符来访问

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

字符串字面量

如果想要在表达式中使用字符串字面量,则属性值需要设置成单引号

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

也可以将字符串使用单引号或者使用`

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

资源

也可以在表达式中访问资源

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

支持字符串的格式化

<string name="info">first name is %1$s last name is %2$s age is %3$d</string>

<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text='@{@string/info(user.firstName,user.lastName,user.age)}'
        />

在表达式中引用一些资源与直接引用资源有所不同,需要显示判断类型

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

数据对象

前面已经介绍了如何将绑定对象,但是当数据发生改变时,不会导致UI更新。

数据绑定可以通过以下三种方式实现数据变化通知:

  • Observable对象
  • Observable字段
  • Observable集合

Observable对象

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

Observable接口负责添加和删除监听器,但通知与否由开发人员管理。为了简化开发,创建了一个基类BaseObservable,以实现侦听器注册机制。数据类实现者仍然负责通知属性何时改变。这是通过将Bindable注释分配给getter并在setter中通知的。

public class User extends BaseObservable {
  private String firstName;
  private String lastName;
  private int age;

  public User(String firstName, String lastName, int age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }

  @Bindable public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
    notifyPropertyChanged(BR.firstName);
  }

  @Bindable public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
    notifyPropertyChanged(BR.lastName);
  }

  @Bindable public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
    notifyPropertyChanged(BR.age);
  }
}

在MyHandlers中定义修改年龄的方法

public class MyHandlers {
  public void changeAge(User user){
    user.setAge(user.getAge()+1);
  }
}
<?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="cn.malinkang.databinding.User"
        />
    <variable
        name="handlers"
        type="cn.malinkang.databinding.MyHandlers"
        />

  </data>
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text='@{@string/info(user.firstName,user.lastName,user.age)}'
        />
    <Button
        android:text="Chage Age"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="@{() -> handlers.changeAge(user)}"
        />
  </LinearLayout>
</layout>

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

Observable字段

ObservableField用于将字段包装成一个可观察的对象。数据绑定提供了如下类 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.这些类中都持有一个基本类型,当内部的基本类型发生改变,ui将进行刷新。

public class User  {
  public ObservableField<String> firstName;
  public ObservableField<String> lastName;
  public ObservableInt age;

  public User(String firstName, String lastName, int age) {
    this.firstName = new ObservableField<>(firstName);
    this.lastName = new ObservableField<>(lastName);
    this.age = new ObservableInt(age);
  }
  
}
public class MyHandlers {
  public void changeAge(User user){
    user.age.set(user.age.get()+1);
  }
}

Observable集合

数据绑定提供了ObservableArrayListObservableArrayMap等可观察的集合类,当集合中数据发生改变,则自动刷新数据。

属性 Setters

无论何时绑定值改变,生成的binding类必须调用一个setter方法。Data binding框架有一些方法来定制调用哪个方法来设置值。

自动 Setters

对于一个属性,Data Binding会试图寻找属性相对应的set方法。例如TextView的android:text属性的表达式会寻找一个setText的方法。如果相关的set方法没有相应的属性,您可以通过Data Binding轻松地为任何setter“创造”属性。例如,DrawerLayout没有任何属性,但大量的setters。您可以自定义属性来调用相应的set方法。

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

重命名的Setters

一些有setters的属性按名称并不匹配,例如android:tint属性与setImageTintList相关联,而不与setTint相关。BindingMethods可以重新为属性指定方法。

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

自定义Setters

有些属性需要自定义绑定逻辑。例如,对于android:paddingLeft属性并没有相关setter。相反,setPadding(left, top, right, bottom)是存在在。BindingAdapter注解允许开发者自定义setter方法。

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

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

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

@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView 
app:imageUrl=“@{venue.imageUrl}”
app:error=“@{@drawable/venueError}”/>

常见问题

参考

开源库

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

推荐阅读更多精彩内容