Android之DataBinding学习笔记
简介
Data binding 在2015年7月发布的Android Studio v1.3.0 版本上引入,在2016年4月Android Studio v2.0.0 上正式支持。目前为止,Data Binding 已经支持双向绑定了。
Databinding 是一个实现数据和UI绑定的框架,是一个实现 MVVM 模式的工具,有了 Data Binding,在Android中也可以很方便的实现MVVM开发模式。
Data Binding 是一个support库,最低支持到Android 2.1(API Level 7+)。
Data Binding 之前,我们不可避免地要编写大量的重复的代码,如 findViewById()、setText(),setVisibility(),setEnabled() 或 setOnClickListener() 等,通过 Data Binding , 我们可以通过声明式布局以精简的代码来绑定应用程序逻辑和布局,这样就不用编写大量的重复的代码了。
studio环境构建
1.在模块的build.gradle文件中添加dataBinding配置
android {
......
dataBinding{
enabled = true
}
......
}
注意:如果app依赖了一个使用 Data Binding 的库,那么app module 的 build.gradle 也必须配置 Data Binding
Data Binding 布局文件 - (View)
Data binding 的布局文件与传统布局文件有一点不同。它以一个 layout 标签作为根节点,里面是 data 标签与 view 标签。view 标签的内容就是不使用 Data Binding 时的普通布局文件内容。以下是一个例子:
activity_main.xml
<?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="entry"
type="cn.itrealman.databindingdemo.Entry"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cn.itrealman.databindingdemo.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20sp"
android:text="@{entry.text}"
android:textColor="@{entry.color}"/>
</RelativeLayout>
</layout>
在上面的布局中,对于data里面的variable nama=”entry”表示的是一个变量名entry,type表示你所对应实体的包名路径,在TextView中,我们采用@{}的语法来引用实体类Entry中的属性。
数据对象 - (Model)
Entry.java
public class Entry {
private String text;
private int color;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
}
上面就是我们所说的实体类Entry,用于TextView的android:text
属性的表达式@{entry.text}
和android:textColor
属性表达式@{entry.color}
绑定数据 - (ViewModel)
在默认情况下,会基于布局文件生成一个继承于 ViewDataBinding 的 Binding 类,将它转换成和你的布局命名并在名字后面接上Binding。例如,布局文件叫 activity_main.xml,所以会生成一个 ActivityMainBinding类。这个类包含了布局文件中所有的绑定关系,会根据绑定表达式给布局文件赋值。在 inflate 的时候创建 binding 的方法如下:
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
Entry entry = new Entry();
entry.setText("文本数据1");
entry.setColor(0xff0000ff);
binding.setEntry(entry);
}
}
事件处理
类似于 android:onClick 可以指定 Activity 中的函数,Data Binding 也允许处理从视图中发送的事件。
有两种实现方式:
方法调用
监听绑定
二者主要区别在于方法调用在编译时处理,而监听绑定于事件发生时处理。
方法调用
相较于 android:onClick ,它的优势在于表达式会在编译时处理,如果函数不存在或者函数签名不对,编译将会报错。
以下是个例子:
Entry.java
public class Entry {
private String text;
private int color;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
//在这个实体中添加一个点击时间的方法
public void onClick(View view){
Toast.makeText(view.getContext(),"已点击",Toast.LENGTH_SHORT).show();
}
}
activity_main.xml
<?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="entry"
type="cn.itrealman.databindingdemo.Entry"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cn.itrealman.databindingdemo.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20sp"
android:text="@{entry.text}"
android:textColor="@{entry.color}"
android:onClick="@{entry::onClick}"/>
</RelativeLayout>
</layout>
通过在上面的TextView中添加一个android:onClick
属性表达式为@{entry.onClick}
这样就能调用点击事件 了,(注意:对应的方法名和监听器对象必须对应) 如果该方法不存在,则在编译的时候就不会通过了。除了上面的表达式可以表示之外,我们还可以使用以下的表达式方式进行设置:
android:onClick="@{entry.onClick}"
(不过这种方式已经过时了,因为这样的表示会和entry.text这样的属性引用难以区分)
监听绑定
监听绑定在事件发生时调用,可以使用任意表达式
此功能在 Android Gradle Plugin version 2.0 或更新版本上可用.
在方法引用中,方法的参数必须与监听器对象的参数相匹配。在监听绑定中,只要返回值与监听器对象的预期返回值相匹配即可。
Entry.java
public class Entry {
private String text;
private int color;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
//将点击监听事件中添加了一个参数
public void onClick(View view,String str){
Toast.makeText(view.getContext(),"已点击,产生了" + str,Toast.LENGTH_SHORT).show();
}
}
activity_main.xml
<?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="entry"
type="cn.itrealman.databindingdemo.Entry"/>
<variable
name="str"
type="String"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cn.itrealman.databindingdemo.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20sp"
android:text="@{entry.text}"
android:textColor="@{entry.color}"
android:onClick="@{(v) -> entry.onClick(v,str)}"/>
</RelativeLayout>
</layout>
MainAcitivty.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_main,new MyComponent(this));
Entry entry = new Entry();
entry.setText("文本数据1");
entry.setColor(0xff0000ff);
//设置测试字符串
binding.setStr("我是监听绑定的数据测试");
binding.setEntry(entry);
}
}
当一个回调函数在表达式中使用时,数据绑定会自动为事件创建必要的监听器并注册监听。关于android:onClick="@{(v) -> entry.onClick(v,str)}"
这里采用的是java1.8中的lambda表达式的形式,因为这样我们可以传递相应的参数进去。
导入(Imports)
1.data 标签内可以有多个 import 标签。你可以在布局文件中像使用 Java 一样导入引用
activity_main.xml
<?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>
<import type="android.view.View"/>
<variable
name="show"
type="boolean"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cn.itrealman.databindingdemo.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="@{show ? View.VISIBLE : View.GONE}"
android:textSize="20sp"/>
</RelativeLayout>
</layout>
2.当类名发生冲突时,可以使用 alias
<import type="android.view.View"/>
<import type="cn.itrealman.databindingdemo.utils.View" alias="UtilsView"/>
3.导入的类型也可以用于变量的类型引用和表达式中
<data>
<import type="cn.itrealman.databindingdemo.Entry"/>
<import type="java.util.List"/>
<variable name="entry" type="Entry"/>
<variable name="entryList" type="List<Entry>"/>
</data>
注意:Android Studio 还没有对导入提供自动补全的支持。你的应用还是可以被正常编译,要解决这个问题,你可以在变量定义中使用完整的包名。
4.导入也可以用于在表达式中使用静态方法
StringUtils.java
public class StringUtils {
public static String show(String string){
string = string.toUpperCase();
return string;
}
}
activity_main.xml
<data>
<variable
name="entry"
type="cn.itrealman.databindingdemo.Entry"/>
<import type="cn.itrealman.databindingdemo.utils.StringUtils"/>
</data>
......
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20sp"
android:text="@{StringUtils.show(entry.text)}"
android:textColor="@{entry.color}"/>
</layout>
5.java.lang.* 包中的类会被自动导入,可以直接使用,例如, 要定义一个 String 类型的变量
<variable name="str" type="String" />
变量 Variables
1.data 标签中可以有任意数量的 variable 标签。每个 variable 标签描述了会在 binding 表达式中使用的属性。
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="entry" type="cn.itrealman.databindingdemo.Entry"/>
<variable name="image" type="Drawable"/>
<variable name="str" type="String"/>
</data>
2.可以在表达式中直接引用带 id 的 view,引用时采用驼峰命名法。
<TextView
android:id="@+id/m_entry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={entry.text}" />
<TextView
android:text="@{user.entry}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{mEntry.getVisibility() == View.GONE ? View.GONE : View.VISIBLE}" />
<!-- 这里TextView直接引用第一个TextView,mEntry为m_entry id的驼峰命名,默认会去掉下划线 -->
3.binding 类会生成一个命名为 context 的特殊变量(其实就是 rootView 的 getContext() ) 的返回值),这个变量可用于表达式中。 如果有名为 context 的变量存在,那么生成的这个 context 特殊变量将被覆盖。
StringUtils.java
public class StringUtils {
public static String show(Context context,String string){
Toast.makeText(context, string,Toast.LENGTH_SHORT).show();
string = string.toUpperCase();
return string;
}
}
<data>
<variable
name="entry"
type="cn.itrealman.databindingdemo.Entry"/>
<import type="cn.itrealman.databindingdemo.utils.StringUtils"/>
</data>
......
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20sp"
android:text="@{StringUtils.show(context,entry.text)}"
android:textColor="@{entry.color}"/>
自定义绑定类名
默认情况下,binding 类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加 “Binding” 结尾。这个类会被放置在 databinding 包中。举个例子,布局文件 activity_main.xml 会生成 ActivityMainBinding 类。如果 module 包名为 com.example.my.app ,binding 类会被放在 com.example.my.app.databinding 中。
通过修改 data 标签中的 class 属性,可以修改 Binding 类的命名与位置。举个例子:
<data class="CustomBinding">
...
</data>
以上会在 databinding 包中生成名为 CustomBinding 的 binding 类。如果需要放置在不同的包下,可以在前面加 “.”:
<data class=".CustomBinding">
...
</data>
这样的话, CustomBinding 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中:
<data class="com.example.CustomBinding">
...
</data>
Includes
<?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="entry"
type="cn.itrealman.databindingdemo.Entry"/>
</data>
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/include"
app:entry="@{entry}"/>
</RelativeLayout>
</layout>
include.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="entry"
type="cn.itrealman.databindingdemo.Entry"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{entry.text}"
android:textColor="@{entry.color}">
</TextView>
</layout>
需要注意, activity_main.xml 与 include.xml 中都需要声明 user 变量。
Data binding 不支持直接包含 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="entry"
type="cn.itrealman.databindingdemo.Entry"/>
</data>
<merge>
<include layout="@layout/include"
app:entry="@{entry}"/>
</merge>
</layout>
表达式语言
表达式语言与 Java 表达式有很多相似之处。下面是相同之处:
- 数学计算 + - / * %
- 字符串连接 +
- 逻辑 && ||
- 二进制 & | ^
- 一元 + - ! ~
- 位移 >> >>> <<
- 比较 == > < >= <=
- instanceof
- 组 ()
- 字面量 - 字符,字符串,数字, null
- 类型转换
- 函数调用
- 字段存取
- 数组存取 []
- 三元运算符 ?:
<!-- 内部使用字符串 & 字符拼接-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`num:` + String.valueOf(entry.num)}"/>
<!-- 三目运算-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{show ? View.VISIBLE : View.GONE}"/>
在xml中转义是不可避免的,如 : 使用“&&”是编译不通过的,需要使用转义字符 “&&”
附:常用的转义字符
描述 | 转义字符 | 十进制 | 显示结果 |
---|---|---|---|
空格 |  ; |  ; | |
小于号 | <; | <; | < |
大于号 | >; | >; | > |
与号 | &; | &; | & |
引号 | "; | "; | " |
撇号 | &apos; | '; | ' |
乘号 | ×; | ×; | × |
除号 | ÷; | ÷; | ÷ |
不支持的操作符
一些 Java 中的操作符在表达式语法中不能使用。
- this
- super
- new
- 显示泛型调用<T>
Null合并运算符
Null合并运算符 ?? 会在非 null 的时候选择左边的操作,反之选择右边。
android:text="@{entry.text ?? `Default Text`}"
等同于
android:text="@{entry.text != null ? entry.text : `Default Text`}"
容器类
通用的容器类:数组,List ,SparseArray ,和 Map,可以用 [] 操作符来存取
<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["text"]}'
也可以用双引号将属性包起来。这样的话,字符串常量就可以用 ” 或者反引号 ( ` ) 来调用
android:text="@{map[`text`}"
android:text="@{map["text"]}"
资源
也可以在表达式中使用普通的语法来引用资源:
android:text="@{@string/text(entry.text)"
字符串格式化和复数形式可以这样实现:
android:text="@{@plurals/sample_plurals(num)}"
当复数形式有多个参数时,应该这样写:
android:text="@{@plurals/numbers(num, num)}"
Type | Normal Reference | Expression Reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
数据对象 (Data Objects)
任何 POJO 对象都能用在 Data Binding 中,但是更改 POJO 并不会同步更新 UI。Data Binding 的强大之处就在于它可以让你的数据拥有更新通知的能力。
Observable 对象
Observable 字段
Observable 容器类
当以上的 observable 对象绑定在 UI 上,数据发生变化时,UI 就会同步更新。
Observable 对象
Observable 接口有一个添加/移除 listener 的机制,但通知取决于开发者。为了简化开发,Android 原生提供了一个基类 BaseObservable 来实现 listener 注册机制。这个类也实现了字段变动的通知,只需要在 getter 上使用 Bindable 注解,并在 setter 中通知更新即可。
public class Entry extends BaseObservable{
private String text;
private int color;
@Bindable
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
notifyPropertyChanged(BR.text);
}
@Bindable
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
notifyPropertyChanged(BR.color);
}
public void onClick(View view,String str){
Toast.makeText(view.getContext(),"已点击,产生了" + str,Toast.LENGTH_SHORT).show();
setText(str);
setColor(0xffff0000);
}
}
BR
是编译阶段生成的一个类,功能与 R.java 类似,用 @Bindable 标记过 getter 方法会在 BR 中生成一个 entry。
当点击 事件使数据发生变化时需要调用 notifyPropertyChanged(BR.text)
通知系统 BR.text
这个 entry 的数据已经发生变化以更新UI。
ObservableFields
创建 Observable 类还是需要花费一点时间的,如果想要省时,或者数据类的字段很少的话,可以使用 ObservableField 以及它的派生 ObservableBoolean、ObservableByte 、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble、
ObservableParcelable 。
ObservableFields 是包含 observable 对象的单一字段。原始版本避免了在存取过程中做打包/解包操作。要使用它,在数据类中创建一个 public final 字段:
public class EntryField{
public ObservableField<String> mText = new ObservableField<>();
public ObservableField<Integer> mColor = new ObservableField<>();
public EntryField(String text, int color) {
mText.set(text);
mColor.set(color);
}
}
要存取数据,只需要使用 get() / set() 方法:
mEntryField.mText.set("text");
mEntryField.mColor.set(0xffff0000);
String text = mEntryField.mText.get();
String color = mEntryField.mColor.get();
Observable Collections 容器类
一些应用会使用更加灵活的结构来保持数据。Observable 容器类允许使用 key 来获取这类数据。当 key 是类似 String 的一类引用类型时,使用 ObservableArrayMap 会非常方便。
ObservableArrayMap<String, Object> mEntry = new ObservableArrayMap<>();
mEntry .put("text", "it_real_man");
mEntry .put("color", 0xffff0000);
binding.setEntry(mEntry);
在布局中,可以用 String key 来获取 map 中的数据:
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="entry" type="ObservableMap<String, String>"/>
</data>
…
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{entry["text"]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor='@{entry["color"]}'/>
当 key 是整数类型时,可以使用 ObservableArrayList :
ObservableArrayList<String> sEntry= new ObservableArrayList<>();
sEntry.add("entryData");
sEntry.add("text");
sEntry.add("color");
binding.setSEntry(sEntry);
在布局文件中,使用下标获取列表数据:
<data>
<import type = "android.databinding.ObservableList"/>
<variable
name="sEntry"
type="ObservableList<String>"/>
</data>
…
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{sEntry[0]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{sEntry[1]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{sEntry[2]}'/>