Data Binding使用详解(一)

本文介绍了Data Binding的基本用法,包括一些基本概念、事件的处理及观察者模式的简单应用。

简介

Data Binding有很好的灵活性和兼容性,向后兼容至Android 2.1(API级别7+)。

为了使用Data Binding,需要使用Gradle 1.5.0-alpha1+及Android Studio 1.3+。

构建环境

首先需要在app module下的build.gradle文件下添加data binding的支持:

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

注意:如果你的app module依赖的类库使用了data binding,那你也得在app module中配置data binding。

基础功能

为了使用Data Binding,首先需要修改布局文件,布局文件的根元素需要使用<layout>元素:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable name="user" type="io.github.yuweiguocn.databindingdemo.bean.User"/>
    </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="@{user.name}"
            />

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

    </LinearLayout>
</layout>

当布局文件的根元素使用<layout>元素后,Android Studio就会根据布局文件名自动生成一个Binding类,否则不会作处理。默认情况下生成的Binding类名是根据布局文件名称生成的,大写字母开头,移除下划线并大写后面的字母最后加上“Binding”后缀。这个类会放在module包下的databinding包下。例如,布局文件contact_item.xml会生成ContactItemBinding。如果module包名为 com.example.my.app那它会放在com.example.my.app.databinding包下。这个类控制着布局文件中的所有binding,从布局属性(如:variable变量)到布局View及设置绑定表达式的值。

对于布局中每个设置ID的View会在Binding类中生成对应的public final域,生成规则为View的ID名首字母小写,移除下划线并大写后面的字母。例如,View ID tv_hello会生成tvHello。

对于每个被描述的变量生成的binding类会对应有setter和getter。变量会使用Java默认值直到setter被调用——引用类型为null,int为0,boolean为false,等等。当为了不同的配置(例:横屏或竖屏)有不同的布局文件,这些变量会被合并。这些布局文件中定义的变量不能有冲突。

我们可以通过data元素class属性修改Binding类名或放在不同包下。例如:

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

写在布局文件属性中的表达式使用“@{}” 语法。TextView的文本被设置为了user中的name属性:

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

然后来看一下User类:

public class User {
    public final String name;
    public final String age;

    public User(String name, String age) {
        this.name = name;
        this.age = age;
    }
}

这个对象的类型数据从不会被改变。在应用程序中这是很常见的,数据只读一次之后不会被改变。也可以使用一个JavaBeans对象:

public class User {
    public final String name;
    public final String age;

    public User(String name, String age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public String getAge() {
        return age;
    }
}

这两个类对Data Binding来说是相等的。TextView中的text属性user.name值会访问前面类的name字段和后面类的getName方法。另外如果有name()方法也会访问这个。

逻辑缜密的同学已经考虑到了直接使用user.name若user为null会不会报空指针异常,生成的Binding类会检查null值并且会避免空指针异常。例如,在@{user.name}表达式中,如果user为null,user.name会分配它的默认值(null)。如果引用的是user.age,age是一个int值,那它的默认值为0。

在Activity的onCreate中,我们需要将setContentView换为DataBinding的方式,并且我们需要调用Binding类的setter为<data>元素下的每个变量设置相应的对象:

ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setUser(new User("我是姓名","我是年龄"));

没错,就是这么简单,快来看看效果吧:

example.png

你也可以使用这种方法得到view:

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
View view = binding.getRoot();//获取对应的View

如果你的ListView或RecyclerView adapter的item使用了data binding,你会更喜欢这种方法:

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

前面提到Binding类可以控制所有布局View,我们试着给TextView加一个ID:android:id="@+id/tv_hello",然后在Activity中直接操作TextView,省去了findViewById操作:

binding.tvHello.setText("Hi,I'm from DataBinding.");

当我们使用<include>元素时,例如:

<include
     android:id="@+id/include_toolbar"
     layout="@layout/view_toolbar"/>

view_toolbar.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:minHeight="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

</layout>

我们可以使用类似方式访问view:

setSupportActionBar(binding.includeToolbar.toolbar);

事件处理

Data Binding允许你写表达式处理view分发的事件(如:onClick)。事件属性名称是由监听方法的名称所管理。例如, View.OnLongClickListener 有一个 onLongClick()方法,因此这个事件的属性是android:onLongClick(这时AS会提示“Unknown attribute android:onLongClick more”,然而这并不影响运行)。有两种方法可以处理一个事件:

方法引用(Method References)

事件可以直接绑定到一个处理的方法,就像android:onClick可以在Activity中指定一个方法。它和View#onClick属性比较最主要的优势在于表达式在编译的时候处理,因此如果方法不存在或它的签名不正确,编译会出现错误。

方法引用和监听绑定之间最主要的区别是真正的监听实现是当数据绑定时创建,不是当事件触发时。如果你想要当事件发生时计算表达式,那你应该使用监听绑定。

这里我们实现点击事件的处理,首先需要一个方法的处理类,包含一个点击事件的处理方法,如下:

public class MainClickHandlers {
    public void onClickName(View v) {
    }
}

然后我们需要在布局文件中引入这个类,并为TextView指定点击监听:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="io.github.yuweiguocn.databindingdemo.ui.main.MainClickHandlers"/>
       <variable name="user" type="io.github.yuweiguocn.databindingdemo.bean.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.name}"
           android:onClick="@{handlers::onClickName}"/>
   </LinearLayout>
</layout>

可以看到我们指定点击事件使用的是"@{handlers::onClickName}"语法,当然你也可以使用"@{handlers.onClickName}"。

注意表达式中方法的签名必须和监听对象中的方法签名完全匹配,否则会编译失败。

最后我们同样需要使用Binding类的setter为每个变量设置相应对象:

binding.setHandlers(new MainClickHandlers());

这样我们就完成了使用方法引用的方式实现了View的点击事件处理。

监听绑定(Listener Bindings)

这个是当事件发生时运行lambda表达式。这个和方法引用类似,但这个可以运行任意的data binding表达式。这个特性需要Gradle 2.0+的支持。

在方法引用中,事件监听的参数和方法的参数必须匹配。在监听绑定中,只需要你的返回值匹配监听期望返回的值即可(除非返回值为void)。

首先和方法引用一样,我们需要在一个类中添加一个方法:

public class MainPresenter implements MainContract.Presenter {
    public void onSaveClick(User user) {//save the user
    }
}

然后在布局文件中引用这个类并调用其中的方法:

<?xml version="1.0" encoding="utf-8"?>
  <layout xmlns:android="http://schemas.android.com/apk/res/android">
      <data>
          <variable name="user"
            type="io.github.yuweiguocn.databindingdemo.bean.User"/>
          <variable name="presenter"
              type="io.github.yuweiguocn.databindingdemo.ui.main.MainPresenter"/>
      </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.age}"
              android:onClick="@{() -> presenter.onSaveClick(user)}"/>
      </LinearLayout>
  </layout>

最后我们同样需要使用Binding类的setter为每个变量设置相应对象:

binding.setPresenter(new MainPresenter());

这样我们就完成了使用监听绑定的方式实现了View的点击事件的处理。使用lambda表达式代表的监听只能作为表达式的顶级元素。当表达式中使用了回调,Data Binding会自动创建必要的监听并且会为事件进行注册。

表达式

在上面的例子中我们没有定义view参数传递到 onClick(android.view.View)。监听绑定为监听参数提供了两个选择:你可以忽略方法的所有参数或方法名称。如果你更喜欢加上参数名称,上面的表达式也可以这样写:

android:onClick="@{(view) -> presenter.onSaveClick(user)}"

如果你想要使用参数名称,你也可以这样写:

public class MainPresenter implements MainContract.Presenter {
    public void onClick(View view) {
    }
}
android:onClick="@{(view) -> presenter.onClick(view)}"

lambda表达式也支持使用多个参数,例如:

public class Presenter {
    public void onCompletedChanged(User user, boolean completed){}
}
<CheckBox
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onCheckedChanged="@{(cb, isChecked) -> presenter.onCompletedChanged(user,isChecked)}"
    />

如果监听的事件返回的是非void值,那么你的表达式也必须返回相同的类型。例如,如果你想监听long click事件,你的表达式应该返回boolean。

public class Presenter {
    public boolean onLongClick(View view){}
}
 android:onLongClick="@{(view) -> presenter.onLongClick(view)}"

如果由于对象为null不能计算表达式,Data Binding会返回Java类型的默认值。例如,引用类型为null,int为0,boolean为false,等等。

表达式也支持三元运算符,例如:

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

导入

在data元素中可以使用零个或多个import元素。这允许你在布局文件中可以很方便的引用类,就像在Java中。

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

现在,View可以用在绑定表达式中,我们可以根据boolean值变量的值决定View是否显示:

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

当导入的类名有冲突时,可以使用“alias”设置别名,这样我们就可以使用Vista代表我们自己定义的类了:

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

变量和表达式可以引用导入的类型:

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

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userList[0].name}"
    />

注意:数组和泛型,比如Observable类,当没有错误的时候可能会显示错误。

在表达式中也可以使用导入的类型的静态域和静态方法:

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

变量

在data元素中可以使用零个或多个variable元素。在binding表达式中可以使用生成的特殊的变量名为“context”。“context”的值是从根View's getContext()获取的Context。“context”变量会被声明的同名变量重写。在编译的时候会检查变量类型,因此如果一个变量实现了Observable或 observable collection,应在类型中得到反映。如果变量是一个基类或没有实现Observable的接口,那么变量不会被观察。

因此,当我们需要使用Context时,上面的例子也可以改成这样:

public class Presenter {
    public void onCompletedChanged(Context ct,boolean completed){
      //use context show toast
    }
}
<CheckBox
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onCheckedChanged="@{(cb, isChecked) -> presenter.onCompletedChanged(context,isChecked)}"
    />

databinding包中给我们提供了很多实现Observable接口的类,使用下面的类型时,当数据更新时会自动更新UI内容:

ObservableArrayList
ObservableArrayMap
ObservableBoolean
ObservableByte
ObservableChar
ObservableDouble
ObservableField
ObservableFloat
ObservableInt
ObservableLong
ObservableParcelable
ObservableShort

我们来定义一个ObservableBoolean类型的变量:

public class User {
    public final String name;
    public final String age;
    public ObservableBoolean isComplete;

    public User(String name, String age, boolean isComplete) {
        this.name = name;
        this.age = age;
        this.isComplete = new ObservableBoolean(isComplete);
    }
}

我们根据CheckBox的事件中修改变量的值:

public class Presenter {
    public void onCompletedChanged(User user, boolean completed){
      user.isComplete.set(completed);
    }
}

在布局文件中通过这个变量的值来确定View是否显示:

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

运行Demo,我们可以看到View随着变量值的改变显示或隐藏(竟然不支持gif):

example.png

避免复杂的监听

监听表达式是非常强大的并且可以让你的代码变得很易于阅读。从另一方面来说,包含复杂表达式的布局会变得很难阅读和维护。这些表达式应该尽可能简单地传递数据从UI到回调方法。你应该在回调方法中实现业务逻辑从监听表达式调用。
存在一些专门点击事件的处理者并且为了避免冲突它们需要一个和android:onClick不一样的属性。为了避免这样冲突创建了下列属性:

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

Includes

通过使用应用的命名空间和变量名,变量可以从containing的布局传递到included的布局binding中:

<?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不支持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>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

完整示例代码可以从这里找到:https://github.com/yuweiguocn/DataBindingDemo

参考链接:https://developer.android.com/topic/libraries/data-binding/index.html

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

推荐阅读更多精彩内容