本文介绍了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("我是姓名","我是年龄"));
没错,就是这么简单,快来看看效果吧:
你也可以使用这种方法得到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):
避免复杂的监听
监听表达式是非常强大的并且可以让你的代码变得很易于阅读。从另一方面来说,包含复杂表达式的布局会变得很难阅读和维护。这些表达式应该尽可能简单地传递数据从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