阅读指南
简介
databinding,实现数据绑定的一种框架,可以降低Activity布局和逻辑的耦合性,省略传统的findViewById操作,节省代码,其中双向绑定,或者单项绑定的操作,可以大量减少传统赋值的代码,有效防止内存泄露,而且能自动规避空指针异常。
优势
- 省略findViewById()操作,且性能更优越,节省代码量
- 能根据数据变化即时更新layout中的控件
- 支持双向更新数据,用于适配器中可以简化刷新逻辑的代码
- 支持Bean中单个字段的更新,节省性能消耗
不足
- Xml中的代码过于繁琐,臃肿!
一、引用方式和Activity基础赋值
1.引入库
android {
dataBinding {
enabled = true
}
}
2.修改layout布局
其中Alt+回车键可以修改成databinding的布局格式
修改之后得结果如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>//用于声明要用到的变量以及变量类型
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TestActivity2">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
3.新建一个Student对象
public class Student {
private int age;
private String name;
...省略get()、set()
}
4.并且在data标签中引用
<data>
//引用方式一
<variable
name="student"
type="com.example.datebindingdemo.Student" />
//引用方式二
<import type="com.example.datebindingdemo.Student"/>
<variable
name="student"
type="Student" />
//如果项目中有两个Student类,那么可以设置一个alisa别名作为区分
<import type="com.example.datebindingdemo.bean.Student" alias="Student2"/>
<variable
name="student2"
type="Student2" />
</data>
5.利用student对象给布局中的控件赋值
<androidx.constraintlayout.widget.ConstraintLayout
...>
<TextView
android:id="@+id/name_tv"
...
android:text="@{student.name,default=小米}"/>
//TextView 引用到相关的变量,DataBinding 会将之映射到相应的getter 方法
//default 属性可以用于设立默认值
<TextView
android:id="@+id/age_tv"
...
android:text="@{String.valueOf(student.age)}"/>
<TextView
android:id="@+id/other_tv"
android:text="" />
</androidx.constraintlayout.widget.ConstraintLayout>
6.设置数据源:
public class TestActivity2 extends AppCompatActivity {
Student student;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//摒弃原有的设置布局方式,通过DataBindingUtil设置
//setContentView(R.layout.activity_test2);
//ActivityTest2Binding是根据layout名字生成
ActivityTest2Binding activityMain2Binding = DataBindingUtil.setContentView(this, R.layout.activity_test2);
student = new Student();
student.setAge(10);
student.setName("小红");
activityMain2Binding.setStudent(student);
//代替findViewById()
activityMain2Binding.otherTv.setText("哈哈");
}
}
二、单向赋值
BaseObservable
Bean对象继承BaseObservable,通过notifyChange()和notifyPropertyChanged()实现单向赋值, addOnPropertyChangedCallback()方法用于监听赋值的变化:
public class Student extends BaseObservable {
//如果是 private 修饰符,则在成员变量的 get 方法上添加 @Bindable 注解
private int age;
//如果是 public 修饰符,则可以直接在成员变量上方加上 @Bindable 注解
@Bindable
public String name;
@Bindable
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
//只更新本字段,BR 的生成通过注释 @Bindable 生成,可以通过 BR notify 特定属性关联的视图
notifyPropertyChanged(BR.age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
//更新所有字段
notifyChange();
}
}
Activity中实现赋值逻辑:
Student student;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
ActivityTest2Binding activityMain2Binding = DataBindingUtil.setContentView(this, R.layout.activity_test2);
student = new Student();
activityMain2Binding.setStudent(student);
//对点击事件赋值
activityMain2Binding.setChange(new Change());
//实现了 Observable 接口的类允许注册一个监听器,当可观察对象的属性更改时就会通知这个监听器
student.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override //propertyId用于识别字段
public void onPropertyChanged(Observable sender, int propertyId) {
if(propertyId==BR.age){
Log.e("改变的是age:",BR.age+"");
}else if(propertyId==BR.name){
Log.e("改变的是name:",BR.name+"");
}else if(propertyId==BR._all){
Log.e("全都改变了",BR._all+"");
}
}
});
}
//实现点击事件的方法类
public class Change{
public void changeAgeListener(){
student.setAge(100);
}
public void changeAll(){
student.setAge(120);
student.setName("洗洗");
}
}
Xml文件中引入点击事件的变量:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.example.datebindingdemo.Student"/>
<variable
name="student"
type="Student" />
<variable
name="change"
type="com.example.datebindingdemo.TestActivity2.Change"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<TextView
android:id="@+id/name_tv"
android:text="@{student.name,default=小小}"
.../>
<TextView
android:id="@+id/age_tv"
android:text="@{String.valueOf(student.age)}"
.../>
<TextView
android:id="@+id/other_tv"
android:text="" />
<Button
android:id="@+id/changeBtn1"
android:onClick="@{()->change.changeAgeListener()}"
...
android:text="改变age"/>
<Button
android:id="@+id/changeBtn2"
android:onClick="@{()->change.changeAll()}"
...
android:text="改变所有属性"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ObservableField
ObservableField可以理解为对BaseObservable的简单化操作,封装了设置每一个字段更新的冗余操作。对java的八大基本类型也有对应的属性调用ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble 以及 ObservableParcelable。
实现效果同BaseObservable一样。
public class Student {
private ObservableInt age;
private ObservableField<String> name;
public ObservableInt getAge() {
return age;
}
public void setAge(ObservableInt age) {
this.age = age;
}
public ObservableField<String> getName() {
return name;
}
public void setName(ObservableField<String> name) {
this.name = name;
}
}
ObservableCollection
dataBinding 也提供了包装类用于替代原生的 List 和 Map,分别是 ObservableList 和 ObservableMap,当其包含的数据发生变化时,绑定的视图也会随之进行刷新
<?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.databinding.ObservableList"/>
<import type="android.databinding.ObservableMap"/>
<variable
name="list"
type="ObservableList<String>"/>
<variable
name="map"
type="ObservableMap<String,String>"/>
<variable
name="index"
type="int"/>
<variable
name="key"
type="String"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.leavesc.databinding_demo.Main12Activity">
<TextView
···
android:padding="20dp"
android:text="@{list[index],default=xx}"/>
<TextView
···
android:layout_marginTop="20dp"
android:padding="20dp"
android:text="@{map[key],default=yy}"/>
<Button
···
android:onClick="onClick"
android:text="改变数据"/>
</LinearLayout>
</layout>
private ObservableMap<String, String> map;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMain12Binding activityMain12Binding = DataBindingUtil.setContentView(this, R.layout.activity_main12);
map = new ObservableArrayMap<>();
map.put("name", "leavesC");
map.put("age", "24");
activityMain12Binding.setMap(map);
ObservableList<String> list = new ObservableArrayList<>();
list.add("Ye");
list.add("leavesC");
activityMain12Binding.setList(list);
activityMain12Binding.setIndex(0);
activityMain12Binding.setKey("name");
}
public void onClick(View view) {
map.put("name", "leavesC,hi" + new Random().nextInt(100));
}
三、双向赋值
双向绑定的意思即为当数据改变时同时使视图刷新,而视图改变时也可以同时改变数据,绑定变量的方式只比单向绑定多了个“=”而已 ,其他操作同单向一样。
<?xml version="1.0" encoding="utf-8"?>
<layout
...>
<data>
<import type="com.example.datebindingdemo.Student"/>
<variable
name="student"
type="Student" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<TextView
android:id="@+id/textView"
android:text="@{student.name}"
... />
<EditText
android:id="@+id/button"
android:text="@={student.name}"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
public class TestActivity3 extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityTest3Binding activityTest3Binding = DataBindingUtil.setContentView(this,R.layout.activity_test3);
Student student = new Student();
activityTest3Binding.setStudent(student);
}
}
四、事件绑定和使用类方法
严格意义上来说,事件绑定也是一种变量绑定,只不过设置的变量是回调接口而已
事件绑定可用于以下多种回调事件
android:onClick
android:onLongClick
android:afterTextChanged
android:onTextChanged
…
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.example.datebindingdemo.Student"/>
<variable
name="student"
type="Student" />
<import type="com.example.datebindingdemo.BindThingActivity.BindThing"/>
<variable
name="bindThing"
type="BindThing" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".BindThingActivity">
<TextView
android:id="@+id/name_tv"
android:layout_width="wrap_content"
//用到了 Lambda 表达式,这样就可以不遵循默认的方法签名,将userInfo对象直接传回点击方法中
android:onClick="@{()->bindThing.onNameClick(student)}"
android:text="@{student.name,default=夏夏}"
android:layout_height="30dp"/>
<TextView
android:id="@+id/age_tv"
android:layout_width="wrap_content"
android:text="@{String.valueOf(student.age)}"
android:layout_height="30dp"/>
<EditText
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@color/colorAccent"
android:hint="用户名"
android:afterTextChanged="@{bindThing.afterNameChanged}"
//此时方法名可以不一样,但方法参数和返回值必须和原始的回调函数保持一致
/>
<EditText
android:layout_width="match_parent"
android:layout_height="30dp"
android:hint="年龄"
android:background="@color/colorAccent"
android:afterTextChanged="@{bindThing.afterAgeChanged}"
/>
</LinearLayout>
</layout>
public class BindThingActivity extends AppCompatActivity {
Student student;
ActivityBindThingBinding activityBindThingBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityBindThingBinding = DataBindingUtil.setContentView(this,R.layout.activity_bind_thing);
student = new Student();
activityBindThingBinding.setStudent(student);
activityBindThingBinding.setBindThing(new BindThing());
}
public class BindThing{
public void onNameClick(Student s){
Toast.makeText(getApplicationContext(),s.getName(),Toast.LENGTH_SHORT).show();
}
public void afterNameChanged(Editable editable){
student.setName(editable.toString());
activityBindThingBinding.setStudent(student);
}
public void afterAgeChanged(Editable editable){
student.setAge(Integer.valueOf(editable.toString()));
activityBindThingBinding.setStudent(student);
}
}
}
使用类方法和事件绑定类似
首先定义一个方法
public class StringUtils {
public static String toUpperCase(String str) {
return str.toUpperCase();
}
}
在 data 标签中导入该类
<import type="com.leavesc.databinding_demo.StringUtils" />
然后调用
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->userPresenter.onUserNameClick(userInfo)}"
android:text="@{StringUtils.toUpperCase(userInfo.name)}" />
五、运算符
DataBinding 支持在布局文件中使用以下运算符、表达式和关键字
算术 + - / * %
字符串合并 +
逻辑 && ||
二元 & | ^
一元 + - ! ~
移位 >> >>> <<
比较 == > < >= <=
Instanceof
Grouping ()
character, String, numeric, null
Cast
方法调用
Field 访问
Array 访问 []
三元 ?:
目前不支持以下操作
this
super
new
显示泛型调用
此外,DataBinding 还支持以下几种形式的调用
Null Coalescing
空合并运算符 ?? 会取第一个不为 null 的值作为返回值
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.name ?? user.password}" />
等价于
android:text="@{user.name != null ? user.name : user.password}"
属性控制:可以通过变量值来控制 View 的属性
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="可见性变化"
android:visibility="@{user.male ? View.VISIBLE : View.GONE}" />
避免空指针异常
DataBinding 也会自动帮助我们避免空指针异常
例如,如果 “@{userInfo.password}” 中 userInfo 为 null 的话,userInfo.password 会被赋值为默认值 null,而不会抛出空指针异常
六、include 和 viewStub
include
对于 include 的布局文件,一样是支持通过 dataBinding 来进行数据绑定,此时一样需要在待 include 的布局中依然使用 layout 标签,声明需要使用到的变量
view_include.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.leavesc.databinding_demo.model.User" />
<variable
name="userInfo"
type="User" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#acc">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="20dp"
android:text="@{userInfo.name}" />
</android.support.constraint.ConstraintLayout>
</layout>
在主布局文件中将相应的变量传递给 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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.leavesc.databinding_demo.model.User" />
<variable
name="userInfo"
type="User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".Main6Activity">
<include
layout="@layout/view_include"
bind:userInfo="@{userInfo}" />
</LinearLayout>
</layout>
viewStub
在布局文件中引用 viewStub 布局
<ViewStub
android:id="@+id/view_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/view_stub"/>
获取到 ViewStub 对象,由此就可以来控制 ViewStub 的可见性
ActivityMain6Binding activityMain6Binding = DataBindingUtil.setContentView(this, R.layout.activity_main6);
View view = activityMain6Binding.viewStub.getViewStub().inflate();
如果需要为 ViewStub 绑定变量值,则 ViewStub 文件一样要使用 layout 标签进行布局,主布局文件使用自定义的 bind 命名空间将变量传递给 ViewStub
<ViewStub
android:id="@+id/view_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/view_stub"
bind:userInfo="@{userInfo}" />
如果在 xml 中没有使用 bind:userInfo="@{userInf}"对 ViewStub 进行数据绑定,则可以等到当 ViewStub Inflate 时再绑定变量,此时需要为 ViewStub 设置 setOnInflateListener回调函数,在回调函数中进行数据绑定
activityMain6Binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
//如果在 xml 中没有使用 bind:userInfo="@{userInf}" 对 viewStub 进行数据绑定
//那么可以在此处进行手动绑定
ViewStubBinding viewStubBinding = DataBindingUtil.bind(inflated);
viewStubBinding.setUserInfo(user);
Log.e(TAG, "onInflate");
}
});
七、BindingAdapter
BindingAdapter用来设置布局中View的自定义属性,当使用该属性时,可以自定义其行为。
用于支持自定义属性,或者是修改原有属性。注解值可以是已有的 xml 属性,例如 android:src、android:text等,也可以自定义属性然后在 xml 中使用
图片下载小例子
当对象的url地址发生改变,会及时更新图片的下载链接,首先定义一个类用于调用我们自定义的属性
public class CustomBindMethod {
//方法必须设置成static静态的
//android:imageUrl属性用于在xml中设置绑定数据源
@BindingAdapter("android:imageUrl")
public static void bindImageUrl(ImageView view, String imageUrl){
//可以在此处下载图片,或者执行其他逻辑
if(null!=imageUrl){
Log.e("改变的Url地址",imageUrl);
}
}
//改变默认属性。整个工程中使用到了 "android:text" 这个属性的控件,其显示的文本就会多出一个后缀
@BindingAdapter("android:text")
public static void setText(Button view, String text) {
view.setText(text + "-后缀");
}
}
xml中配置自定义的属性值:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.example.datebindingdemo.ImageBean"/>
<variable
name="imageBean"
type="ImageBean" />
<import type="com.example.datebindingdemo.BindingAdapterActivity.ChangeUrl"/>
<variable
name="changeUrl"
type="ChangeUrl" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
tools:context=".BindingAdapterActivity">
<ImageView
android:id="@+id/image"
android:layout_width="300dp"
android:imageUrl="@{imageBean.url}"
android:layout_height="300dp"/>
<Button
android:id="@+id/changeBtn"
android:layout_width="match_parent"
android:layout_height="40dp"
android:onClick="@{()->changeUrl.changeMethod(imageBean)}"
android:text='@{"改变图片Url"}'/>
</LinearLayout>
</layout>
在Activity中设置图片下载链接
public class BindingAdapterActivity extends AppCompatActivity {
ImageBean imageBean;
ActivityBindingAdapterBinding adapterBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
adapterBinding = DataBindingUtil.setContentView(this,R.layout.activity_binding_adapter);
imageBean = new ImageBean();
adapterBinding.setImageBean(imageBean);
adapterBinding.setChangeUrl(new ChangeUrl());
}
public class ChangeUrl{
public void changeMethod(ImageBean bean){
bean.setUrl("图片下载地址");
CustomBindMethod.bindImageUrl(adapterBinding.image,bean.getUrl());
}
}
}
八、 BindingConversion
对数据进行转换,或者进行类型转换,或者用于转换属性值的类型。
对相同属性的数据进行修改
还是引用BindingAdapter章节的例子,在CustomBindMethod类中设置如下方法:
//以下方法会将布局文件中所有以@{String}方式引用到的String类型变量加上后缀-conversionString
@BindingConversion
public static String conversionString(String text) {
return text + "-conversionString";
}
在xml中增加如下属性:
<TextView
android:layout_width="wrap_content"
android:text='@{"xxx"}'
android:layout_height="wrap_content"/>
得到的效果如下:
发现,连同button的“android:text”属性也修改了,实际上BindingConversion比BindingAdapter优先级更高。
转换属性的小例子
看以下布局,此处在向 background 和 textColor 两个属性赋值时,直接就使用了字符串,按正常情况来说这自然是会报错的,但有了 BindingConversion 后就可以自动将字符串类型的值转为需要的 Drawable 和 Color 了
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background='@{"红色"}'
android:padding="20dp"
android:text="红色背景蓝色字"
android:textColor='@{"蓝色"}'/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background='@{"蓝色"}'
android:padding="20dp"
android:text="蓝色背景红色字"
android:textColor='@{"红色"}'/>
@BindingConversion
public static Drawable convertStringToDrawable(String str) {
if (str.equals("红色")) {
return new ColorDrawable(Color.parseColor("#FF4081"));
}
if (str.equals("蓝色")) {
return new ColorDrawable(Color.parseColor("#3F51B5"));
}
return new ColorDrawable(Color.parseColor("#344567"));
}
@BindingConversion
public static int convertStringToColor(String str) {
if (str.equals("红色")) {
return Color.parseColor("#FF4081");
}
if (str.equals("蓝色")) {
return Color.parseColor("#3F51B5");
}
return Color.parseColor("#344567");
}
效果如下:
九、Array、List、Set、Map …
dataBinding 也支持在布局文件中使用 数组、Lsit、Set 和 Map,且在布局文件中都可以通过 list[index] 的形式来获取元素
而为了和 variable 标签的尖括号区分开,在声明 Lsit< String > 之类的数据类型时,需要使用尖括号的转义字符
<?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="java.util.List" />
<import type="java.util.Map" />
<import type="java.util.Set" />
<import type="android.util.SparseArray" />
<variable
name="array"
type="String[]" />
<variable
name="list"
type="List<String>" />
<variable
name="map"
type="Map<String, String>" />
<variable
name="set"
type="Set<String>" />
<variable
name="sparse"
type="SparseArray<String>" />
<variable
name="index"
type="int" />
<variable
name="key"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".Main7Activity">
<TextView
···
android:text="@{array[1]}" />
<TextView
···
android:text="@{sparse[index]}" />
<TextView
···
android:text="@{list[index]}" />
<TextView
···
android:text="@{map[key]}" />
<TextView
···
android:text='@{map["leavesC"]}' />
<TextView
···
android:text='@{set.contains("xxx")?"xxx":key}' />
</LinearLayout>
</layout>
十、资源引用
dataBinding 支持对尺寸和字符串这类资源的访问
dimens.xml
<dimen name="paddingBig">190dp</dimen>
<dimen name="paddingSmall">150dp</dimen>
strings.xml
<string name="format">%s is %s</string>
<data>
<variable
name="flag"
type="boolean" />
</data>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@{flag ? @dimen/paddingBig:@dimen/paddingSmall}"
android:text='@{@string/format("leavesC", "Ye")}'
android:textAllCaps="false" />
感谢
感谢以下博文的指导,其中为了节约博文的时间,引用了作者的部分文字,非常感激!
Android DataBinding 从入门到进阶