概念
DataBinding是Google为了Android能够更好地实现MVVM架构而设计的,可以减轻页面(Activity/Fragment)的工作量,让布局承担一部分原本属于页面的工作。有如下特点:
- 项目更简洁,可读性更高
- 不再需要findViewById
- UI控件能直接与数据模型中的字段绑定,甚至能响应用户的交互
基本使用
- 在app的build.gradle中启用数据绑定
android {
......
dataBinding{
enabled = true
}
}- 修改布局文件
<?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">
<data>
<variable
name="personInfo"
type="com.json.jetpack.databinding.Person" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="@{personInfo.name}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_age"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="60dp"
android:text="@{String.valueOf(personInfo.age)}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name" />
<TextView
android:id="@+id/tv_address"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="60dp"
android:text="@{personInfo.address}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_age" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>撇开布局文件中的@{}语法,可以看到这个与普通的布局文件的区别是在布局文件外层加入了layout标签 - 可以手动修改布局文件,为其添加<layout>标签,也可以将鼠标移动至布局文件根目录位置,单击浮现出来的菜单,选择“convert to data binding layout”选项,AS会自动添加layout/data标签。
添加layout标签的目的是告诉DataBinding库,我们希望对该布局文件实行绑定。此时rebuild项目,DataBinding库会自动生成绑定该布局文件所需要的类 - 生成的类名规则是:布局文件名 + Binding,比如activiy_base_info,则DataBinding库生成的类是ActivityBaseInfoBinding。
- 实例化布局文件
有了DataBinding组件库之后,就可以告别findViewById了,为啥?Data直接绑定到布局了。以前我们通过setContentView来实例化布局文件,现在我们需要通过DataBindingUtil.setContentView方法来实例化布局文件。该方法返回实例化后的布局文件对象。该布局文件对象即是上面提到的DataBindng库自动生成的类。
ActivityBaseInfoBinding activityBaseInfoBinding = DataBindingUtil.setContentView(this, R.layout.activity_base_info);
- 将数据传递到布局文件
<layout>
<data>
<variable
name="personInfo"
type="com.json.jetpack.databinding.Person" />
</data>
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="@{personInfo.name}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
......
</layout>- 首先,在布局文件中定义布局变量<variable>,布局变量指定对象的类型和名字,名字可以自定义。
- 接着,我们便可以在Activity中,通过setVariable方法,将Person对象传递给布局文件了。注意:这里的BR类类似于Android项目中常见的R类。
ActivityBaseInfoBinding activityBaseInfoBinding = DataBindingUtil.setContentView(this, R.layout.activity_base_info);
// 方式1
activityBaseInfoBinding.setVariable(BR.personInfo, mPerson);
// 方式2
activityBaseInfoBinding.setPersonInfo(mPerson);DataBinding为我们提供的以上两种方式将数据与布局绑定。
DataBinding本质上可以看做是一个代码生成器,当你启用了它,它能够自动帮助你生成绑定所需要的相关代码。
<data>标签用于放置布局文件中各个UI控件所需的所有数据。这些数据可以是自定义类型,如Person,也可以是基本类型,如String等
<variable name="message" type="String">- 绑定布局变量与成员变量
DataBinding库提供了布局表达式,布局表达式以@{}的格式作为属性的值存在,使用起来很方便。
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="@{personInfo.name}"
/>绑定后,我们就不需要在Activity中通过setText方法对UI控件进行赋值了。
- 布局文件中引用静态类
<data>
<import type="com.json.jetpack.databinding.MyUtil" />
......
</data>
<TextView
android:id="@+id/tv_address"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="60dp"
android:text="@{MyUtil.getAddress()}"
/>小结:
- 布局文件通过<data>、@{}布局表达式完成数据与控件的绑定关系,通过setXxx或setVaraiable设置最终要显示的数据
- 引用静态类使用import,普通类使用variable
- DataBinding响应事件
- 布局文件
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="60dp"
android:onClick="@{handleListener.clickTest}"
/>
- 事件处理类
public class EventHandlerListener {
public void clickTest(View view) {
Log.e("EventHandlerListener", "我被点击了");
}
}
- MainActivity
mHandleListener = new EventHandlerListener();
activityBaseInfoBinding.setVariable(BR.handleListener, mHandleListener);和普通的数据绑定没啥区别
- 二级页面的绑定 - include进来的布局
二级页面
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="myBook"
type="com.json.jetpack.databinding.Book" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{myBook.name}" />
</RelativeLayout>
</layout>一级页面
<variable
name="book"
type="com.json.jetpack.databinding.Book" />
<!--二级页面数据传递-->
<include
layout="@layout/layout_rl_include"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_info"
app:myBook="@{book}" />BindingAdapter
- 原理
在gradle中启用DataBinding库,它会为我们生成绑定所需的各种类。这其中包含大量针对UI控件的、名为XXXBindingAdapter的类。这些类中包含了各种静态方法,并且在静态方法前都有@BindingAdapter标签,标签中的别名对应于UI控件在布局文件中的属性。
DataBinding库针对系统控件单独出了一个组件 - databinding-adapters,这其中包含全是针对系统控件的各种xxxBindingAdapter, 以TextViewBindingAdapter为例 - 针对android:text属性:
public class TextViewBindingAdapter {
......
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
......
}
DataBinding库以静态方法的形式为UI控件的各个属性绑定了相应的代码。若开发者在UI控件的属性中使用了布局表达式,那么当布局文件被渲染时,属性所绑定的静态方法会被自动调用。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{myBook.name}" />当TextView被渲染时,android:text属性会自动调用TextViewBindingAdapter.setText方法。可见,通过简单的设置,便可以在布局文件中调用所绑定的方法。
- 自定义BindingAdapter
为了让布局文件能够承担更多的工作,处理更复杂的业务逻辑,DataBinding库允许我们自定义BindingAdapter。
注意:
- BindingAdapter中的方法均为静态方法
- 第一个参数是调用者本身
- 第二个参数是布局文件在调用该方法时传递进来的参数
- 在静态方法前需加入@BindingAdapter("属性名")
自定义BindingAdapter分两步:
- 自定义一个符合要求(上述注意点)的xxxBindingAdapter;
- 在布局中使用app:属性名 + 布局表达式 ;PS:在布局中使用布局表达式才可触发调用
示例:
public class MyViewBindingAdapter {
@BindingAdapter("extra")
public static void setExtraInfo(TextView textView, String extraInfo) {
if (textView != null) {
if (textView.getText() != null) {
String content = textView.getText().toString();
textView.setText(String.format("%s%s", content, extraInfo));
} else {
textView.setText(extraInfo);
}
}
}
}
- 布局文件
xmlns:app="http://schemas.android.com/apk/res-auto"
<variable
name="extraInfo"
type="String" />
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="60dp"
android:onClick="@{handleListener.clickTest}"
android:text="@{personInfo.name}"
app:extra="@{extraInfo}"
/>
- Activity
activityBaseInfoBinding.setVariable(BR.extraInfo, " -> 我是extra信息");多参数重载
@BindingAdapter(value = {"extra", "extra2"}, requireAll = false)
public static void setExtraInfo(TextView textView, String extraInfo, String extraInfo2)
{
......
}在@BindingAdapter标签中,方法参数以value={"", ""}的形式存在。变量requireAll用于告诉DataBinding库这些参数是否都是要赋值的, 默认值为true,即全部都需要赋值。
- 可选旧值
BindingAdapter中的方法有一个有趣的功能 - 可选旧值。即可以得到属性的旧值,比如,在设置为新值之前,我们希望获取到之前的旧值,以用于某些判断。
activityBaseInfoBinding.setVariable(BR.extraInfo, " -> 我是extra信息");
findViewById(R.id.tv_age).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
activityBaseInfoBinding.setVariable(BR.extraInfo, " -> 我是新的extra信息");
}
});
@BindingAdapter("extra")
public static void setExtraInfo(TextView textView, String oldValue, String extraInfo) {
Log.e("setExtraInfo", "oldValue -> " + oldValue);
Log.e("setExtraInfo", "newValue -> " + extraInfo);
if (textView != null) {
if (textView.getText() != null) {
String content = textView.getText().toString();
textView.setText(String.format("%s%s", content, extraInfo));
} else {
textView.setText(extraInfo);
}
}
}
Log:
E/setExtraInfo: oldValue -> -> 我是extra信息
E/setExtraInfo: newValue -> -> 我是新的extra信息需要注意的是,使用可选旧值时,方法中的参数顺序需要先写旧值,后写新值。
双向绑定
- 单向绑定和双向绑定
前面我们讲到的都是属于单向绑定,即绑定的数据变化,引起布局内容的变化。
双向绑定除了具备单向绑定的特性之前,还可以做到布局内容的变化,对应的字段也能够自动得到更新。比如EditText,随着字段的变化自动更新控件中的内容,还可以实现当用户修改EditText中的内容时,对应的字段也能够自动得到更新,这就是双向绑定。
- 方式1
- 定义待双向绑定的数据类
/**
* 定义一个双向绑定的类
* PS:无论是单向绑定还是双向绑定,本质都是观察者模式
*/
public class TwoDirectionBindModel extends BaseObservable {// 必须继承自BaseObservable - DataBinding库为了方便实现观察者模式而提供的类
private Address address;
public TwoDirectionBindModel() {
address = new Address();
address.setDetailAddress("China");
}
@Bindable// get方法添加@Bindable注解,这是在告诉编译器,我们希望对这个字段进行双向绑定
public String getAddress() {
return address.getDetailAddress();
}
public void setAddress(String newAddress) {
// 在对字段进行更新之前,需要判断新值与旧值是否不同。因为更新后我们会调用notifyPropertyChanged通知观察者,数据已经更新了,观察者在收到通知后,会对set方法进行调用。
// 因此,如果你没有对值进行判断。那么则会引发循环引用问题
if (!TextUtils.isEmpty(newAddress) && !newAddress.equals(address.getDetailAddress())) {
address.setDetailAddress(newAddress);
notifyPropertyChanged(BR.address);// BaseObservable中的方法
}
}
}
- 布局
<variable
name="directionModel"
type="com.json.jetpack.databinding.TwoDirectionBindModel" />
<EditText
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:text="@={directionModel.address}"// 注意:此处的语法是@={}
/>
- 页面
twoDirectionBindModel = new TwoDirectionBindModel ();
activityBaseInfoBinding.setVariable(BR.directionModel, twoDirectionBindModel );
- 方式2
直接使用ObservableField完成上述定义TwoDirectionBindModel的所有工作:
/**
* 定义一个双向绑定的类
*/
public class TwoDirectionBindModel2 {
private ObservableField<Address> address;
public TwoDirectionBindModel2() {
Address data = new Address();
data.detailAddress = "china";
address = new ObservableField<>();
address.set(data);
}
public String getDetailAddress() {
return address.get().detailAddress;
}
public void setDetailAddress(String data) {
address.get().detailAddress = data;
}
}达到一样的效果。因此,使用起来也是方便了很多。可以看到双向绑定无需在页面中加入额外的代码,耦合度更低。
RecyclerView的绑定机制
在RecyclerView.Adapter的回调方法中,可以利用DataBinding库帮助我们实例化RecyclerView中每个Item的布局文件,进而将布局文件中的控件与List<T>中的类型对象T进行绑定。
主要工作在Adapter中,以及Item的布局文件中,除此之外,用法基本没啥变化。
- item 布局文件(layout_item.xml)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="bookInfo"
type="com.json.jetpack.databinding.Book" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@{bookInfo.name}"
android:textColor="@android:color/holo_red_light"
android:textSize="18sp" />
</LinearLayout>
</layout>DataBinding 会为我们生成LayoutItemBinding.java文件
- Adapter
// RecyclerView的其他用法基本不变,唯一需要改变的绑定数据这块,需下沉到Adapter的ViewHolder中
public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {
private List<Book> mBooks;
public void setBooks(List<Book> books) {
this.mBooks = books;
notifyDataSetChanged();
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// RecyclerView.Adapter的使用变化
LayoutItemBinding layoutItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.layout_item, parent, false);
return new MyViewHolder(layoutItemBinding);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
holder.mLayoutItemBinding.setBookInfo(mBooks.get(position));// 绑定工作交由DataBinding库完成
}
@Override
public int getItemCount() {
return mBooks == null ? 0 : mBooks.size();
}
static class MyViewHolder extends RecyclerView.ViewHolder {
LayoutItemBinding mLayoutItemBinding;
public MyViewHolder(@NonNull LayoutItemBinding layoutItemBinding) {
super(layoutItemBinding.getRoot());
mLayoutItemBinding = layoutItemBinding;
}
}
}- 页面布局文件(部分)
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/et" />- 页面
ActivityBaseInfoBinding activityBaseInfoBinding = DataBindingUtil.setContentView(this, R.layout.activity_base_info);
MyRecyclerAdapter adapter = new MyRecyclerAdapter();
activityBaseInfoBinding.rv.setLayoutManager(new LinearLayoutManager(this));
activityBaseInfoBinding.rv.setAdapter(adapter);
List<Book> books = new ArrayList<>();
Book book = new Book();
book.setName("Jetpack");
books.add(book);
Book book2 = new Book();
book2.setName("Jetpack2");
books.add(book2);
Book book3 = new Book();
book3.setName("Jetpack3");
books.add(book3);
Book book4 = new Book();
book4.setName("Jetpack4");
books.add(book4);
Book book5 = new Book();
book5.setName("Jetpack5");
books.add(book5);
Book book6 = new Book();
book6.setName("Jetpack6");
books.add(book6);
adapter.setBooks(books);完事。