JetPack学习笔记之DataBinding

概念

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()}"
/>

 小结:

  1. 布局文件通过<data>、@{}布局表达式完成数据与控件的绑定关系,通过setXxx或setVaraiable设置最终要显示的数据
  2. 引用静态类使用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。

注意:

  1. BindingAdapter中的方法均为静态方法
  2. 第一个参数是调用者本身
  3. 第二个参数是布局文件在调用该方法时传递进来的参数
  4. 在静态方法前需加入@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);

完事。


版权声明:本文为json_it原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。