JetPack-(6)-DataBinding 数据绑定

1.什么是数据绑定

DataBinding是在xml中以声明的方式,将布局中的UI组件绑定与数据源绑定。它可以引用类,并且使用里面的变量或方法,而且控件自身也可以和bean中的某个字段进行关联(包括单项关联或双向关联),这样做的好处是,当任何一方发生变化的时候,另一方会自动更新自己的值。

相比传统的findViewById()查找控件 然后再设置值。DataBinding就显得简洁很多,使得页面与布局之间的耦合度进一步降低,减少了重复的findViewById这一类的代码。

DataBinding具有如下几点优势:

  1. 项目更加简洁,可读性更高。部分与UI控件相关的代码可以在布局文件中完成
  2. 不再需要findViewById()方法,大量减少了Activity中的代码
  3. 布局文件可以包含简单的业务逻辑。UI控件能够直接与数据模型中的字段绑定,甚至能响应用户的交互
  4. 可以提高应用性能,有利于防止内存泄漏以及避免发送Null指针异常
  5. 可以更好的实现MVVM架构

DataBinding 将 Layout布局文件中的UI组件与数据模型Model进行绑定:

  • 当 用户 通过UI组件 修改数据时,会将数据自动更新到数据模型中
  • 数据模型中的数据改变时,会自动更新到UI组件中。

2.基础使用:

DataBinding的绑定有两种方式,单向数据绑定和双向数据绑定:

  • 单向数据绑定:例如,当Model中的数据改变时,通知改变页面上的值。
  • 双向数据绑定: 当Model中的数据改变时,通知改变页面上的值。当页面上的数据修改之后,通知改变Model中的值。

需要用到的依赖:

在Model内的build.gradle中, android/defaultConfig配置块中,配置如下内容:

ini 复制代码
android {
    defaultConfig {
    
        buildFeatures{
            dataBinding = true
        }
        
    }
}

2.1单向绑定:

2.1.1:定义数据类(Model):

arduino 复制代码
public class Student {
   private String name;
   private int age;

   public Student(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }
}

2.1.2 布局转换

在build.gradle构建脚本中,设置dataBinding{enable = true}后,第一时间点击右上角的SyncNow同步代码,否则下面无法进行布局转换。

在布局文件中,将鼠标放在左上角第一个字符位置,按下 Alt+回车 组合键,

按下Alt + 回车 组合键,会弹出下面的菜单, 有 Convert to data binding layout 选项,选择该选项,就可以键布局文件切换成DataBinding布局文件

如果没有该选项,说明在build.gradle构建脚本中 设置了dataBinding{ eabled = true}后,没有同步代码:

转为DataBinding布局前的结构:

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

</androidx.constraintlayout.widget.ConstraintLayout>

转换为DataBinding布局后的结构:

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>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

2.1.3 在xml中定义data标签,设置数据类型信息

ini 复制代码
<?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>

        <variable
            name="student"
            type="com.wzj.databindingdemo.Student" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/text_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{student.name}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.4"
            tools:text="Tom" />

        <TextView
            android:id="@+id/text_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(student.age)}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/text_name"
            app:layout_constraintVertical_bias="0.2"
            tools:text="18" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

在上述代码中,通过data标签,设置了数据类型信息,

在布局文件中,为组件设置tools:text属性,该属性只能在Design视图中查看,方便开发调试,不会显示在最终的应用中。

在布局文件中,使用@{student.变量名},获取该数据类型对象的成员,并设置到布局组件中

3.1.4 注意事项

需要注意的是,如果字段的类型是int类型,如student.age,设置 android:text属性时,需要将其转换为String类型

3.1.5 效果展示

2.2双向绑定:

双向绑定是建立在单向绑定的基础上,实际开发中用到双向绑定的地方没有单向绑定多。

例如EditText,用户编辑EditText,字段自动同步,当字段有变化时,EditText自动更新

2.2.1 代码示例:

定义一个Student类:

typescript 复制代码
public class Student {
   private String name;
   private int age;

   public Student(String name, int age) {
       this.name = name;
       this.age = age;
   }
   public String getNameText(){
       return "姓名"+name;
   }
   public String getAgeText(){
       return "年龄"+age;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }
}

2.2.2 通过BaseObservable实现双向绑定

在该类中设置了@Bindable注解,只要student对象中的name发生了变化,绑定的组件中的内容就会发生变化

如果要实现通过EditText修改数据模型的效果,需要再实现一个setStudentName()方法,该方法需要与之前使用@Bindable注解修饰的getStudentName()方法对应。

修改后需要调用notifyPropertyChanged(BR.xxx)通知数据模型进行变更 BR类是BaseObservable子类中由@Bindable注解修饰的方法生成的

BR类生成位置在:app\build\generated\ap_generated_sources\debug\out\com.databindingdemo\BR.java

2.2.3 布局文件设置(重点):

在EditText控件赋值时,需要使用 android:text="@={student.studentName}" 进行赋值,注意值为: @={student.studentName},比之前的数据绑定多了一个等号

2.2.4 在Activity中,

在Activity组件中,向布局中设置的对象类型是StudentViewModel类型的,不是Student类型的

2.2.5 执行结果:

逐个字母删除Tom,然后输入Jack

2.3 ObservableField实现数据模型Model与视图View双向绑定(优化上述例子)

在上面的做法中,存在一些弊端,首先我们的类必须继承自BaseObservable,另外,在Getter方法前还需要加上@Bindable标签,告诉DataBinding我们要绑定该字段,最后,还需要再Setter方法中手动调用notifyPropertyChanged()方法以通知观察者。

有一种更加简单的做法。那就是ObservableField,它能将普通对象包装成一个可观察的对象,ObservableField可以用于包装各种基本类型,集合数组,自定义类,实现代码如下:

上述代码,也能实现与BaseObservable双向绑定相同的效果。我们只通过ObservableField对象将数据给包装起来,并为字段编写了Getter和Setter方法。GetStudentName()方法在程序启动时被自动调用,当用户修改EditText内容时,setStudentName()方法被自动调用

DataBinding还提供了可观察集合ObservableArrayMap对象和ObservableArrayList对象

ObservableField与LiveData

我们发现ObservableField的使用方式和作用与LiveData很像。实际上,两者可以替换使用的,二者的区别在于,LiveData与生命周期相关,它通常在ViewModel中使用,并且需要在页面中通过observe()方法对变化进行监听。而双向绑定无需在页面中加入额外的代码,耦合度更低。

3.表达式语言

在布局文件中是通过 @{},语法写入控件中的,比如,TextView文本被设置为Student.的Name属性:

但是表达式的功能不止如此,我们还可以在表达式语言中使用以下运算符和关键字:

  • 算数运算符 + - / * %
  • 字符串连接运算符 +
  • 逻辑运算符 && ||
  • 二元运算符 & | ^
  • 一元运算符 + - ! ~
  • 位移运算符 >> >>> <<
  • 比较运算符 == > < >= <= (注意,<需要转义为<)
  • instanceof
  • 分组运算符()
  • 字面量运算符-字符,字符串,数字,null
  • 类型转换
  • 方法调用
  • 字段访问
  • 数组访问[]
  • 三元运算符 age > 18 ? 未成年 : 已成年

3.字符串拼接函数

我们可以在数据类中定义字符串拼接函数,直接在DataBinding布局文件中,调用字符串拼接函数:

还是Student类, 在DataBinding布局文件中,声明

布局文件中的控件中:

  • 调用@{student.nameText()} 设置 "姓名 : tom"
  • 调用@{student.ageText()} 设置 "年龄 : 18"

4.绑定点击事件函数

首先在Student类中定义如下方法:

然后在xml的TextView的android:onClick属性中,设置@{student.onClick}点击事件

5.导入类(import) 变量,包含(使用include导入二级界面布局)

5.1 导入类 import

导入起始就跟使用类方法差不多,通过导入功能,可以在布局文件中引用类,就像在托管代码一样,可以在data元素下使用多个import元素,也可以不使用。

导入View类可以通过绑定表达式引用该类,如下图展示了如何引用View类的VISIBLE和GONE

注意类型别名

当类名有冲突的时候,其中一个类可以使用别名重命名.使用alias即可

当我们使用的时候,使用 myView 即可

导入其他类

导入的类型可用作变量和表达式中的类型引用。以下显示了用作变量类型的sutdent和List

ini 复制代码
<import type="com.wzj.databindingdemo.Student"/>
<import type="java.util.List"/>
<variable name="student" type="Student" />
<variable name="studentList" type="List&lt;Student" />

5.2 变量

变量就是我们使用的标签引入的属性,变量类型会在编译时进行检查。

ini 复制代码
<data>
      <import type="android.graphics.drawable.Drawable"/>
       <variable name="user" type="com.example.User"/>
       <variable name="image" type="Drawable"/>
       <variable name="note" type="String"/>
</data>

5.3 使用include导入二级界面布局

对于include进行引用的布局文件,我们称为二级页面。DataBinding也支持从一级页面将数据传递到二级页面.

一级页面传递数据 A.xml

二级页面接收数据 B.xml

6.自定义BindingAdapter

借助@BindingAdapter注解,可以将自定义逻辑绑定到DataBinding布局中

例如:为ImageView控件绑定数据,传入一个url网络图片地址,该控件中显示网络图片,如果网络图片加载失败或者为空,则加载默认的本地资源。该操作必须自定义一段代码逻辑进行实现,使用简单的数据绑定无法实现该功能。

6.1 基础使用

首先编写处理图片的BindingAdapter类,在该方法中通过Glide加载网络图片

需要注意的是:ImageViewBindingAdapter中的方法均为静态方法。第一个参数为调用者本身(也就是当前的UI控件),第二个参数是布局文件在调用该方法时传递过来的参数。我们在@BindingAdapter中添加的一个别名image,布局文件正是通过别名来调用该方法的。

在布局文件中调用BindingAdapter

首先,需要在布局变量中定义一个String,用于存放网络图片的地址,然后再ImageView通过别名,及我们再ImageViewBindingAdapter文件中定义好的别名image,来调用静态方法,布局表达式@{}中的参数,则是调用方法时传入的参数

需要注意,我们需要在布局文件最外层包含以下命名空间,才能调用自定义@BindingAdapter标签定义的静态方法:

在Activity中为布局变量赋值,也就是networkImage

结果如下:

6.2 方法重载

刚才我们已经自定义BindingAdapter类,让UI空间能够通过属性设置,来加载网络图片。现在,我们可以通过方法重载,让该静态方法支持显示项目资源文件中的图片,代码如下:

布局文件中:

在Activity中:

这样,BindingAdapter就能够显示项目资源文件中的图片了

6.3 多参数重载

我们可以将上面的两个方法,合并成一个方法,并且将两个参数同时传入方法中,当网络图片地址为空时,则显示imageResource参数所指定的图片:

在@BindingAdapter标签中,方法的别名设置以value={"",""}的形式设置,在该方法中,我设置了两个别名,及通过这两个别名都可以调用该静态方法,变量requireAll用于告诉DataBinding库这些参数是否都要赋值,默认为true即要全部赋值.

布局代码如下:

当networkImage为空时,ImageView会显示localImage所指定的图片.

方法是如何区分传入的资源是网络资源还是本地资源呢?Databinding是根据传递参数的类型来进行区分,比如说: networkImage是网络图片资源,那么就是String类型,方法就知道它传入的是网络资源图片,localImage是本地类型资源,那么就是int类型,方法就知道它传入的是本地资源图片。

6.4 可选旧值

BindingAdapter可以覆盖Android原先的控件属性。比如说,可以设置在每一个TextView的文本都加上后缀: -哈哈哈,代码如下:

xml中:

activity中:

实现效果:

6.5 对象转换

6.5.1 自动转换对象

6.5.2 自定义转换BindingConversion

与BindingAdapter类似 以下方法会将布局文件中所有以@{String},方式引用到的String变量加上后缀 Android

xml文件中的TextView:

最终效果:

注意:@BindingConversion会将布局文件中所有以@{String}方法引用到的String变量加上后缀 Android,但是分以下两种情况:

  1. 如果属性是Android自带的属性。比如说android:text,@BindingAdapter的优先级低于@BindingConversion,会优先调用@BindingConversion注解的方法
  2. 如果属性是自定义的。比如我们上面例子中的图片加载,image别名,这时候@BindingAdapter的优先级会高于@BindingConversion,会优先调用@BindingAdapter注解的方法。

7.DataBinding 在 RecyclerView中使用

7.1定义数据类Student

arduino 复制代码
public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

7.2 定义item布局

ini 复制代码
<?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>

        <variable
            name="student"
            type="com.wzj.databindingdemo.Student" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="50dip">

        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{student.name}"
            android:textSize="24sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.3"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Tom" />

        <TextView
            android:id="@+id/age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(student.age)}"
            android:textSize="24sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.7"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="18" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

7.3 定义适配器Adapter

在DataBinding布局中,绑定了 type="com.wzj.databindingdemo.Student" 类型的数据

数据绑定布局ItemBinding在自定义的ViewHolder类中维护,可以通过该类获取数据绑定布局,并为其设置数据

在onCreateViewHolder中,获取DataBinding布局,并将其设置给自定的ViewHolder

获取DataBinding布局,调用DataBindingUtil.inflate函数,获取item.xml对应的DataBinding布局文件类ItemBinding实例对象。

在onBindingViewHolder中

scala 复制代码
public class StudentAdapter extends RecyclerView.Adapter<StudentAdapter.ViewHolder> {

    private List<Student> list = Arrays.asList(
            new Student("Tom", 18),
            new Student("Jerry", 12),
            new Student("Mickey", 16),
            new Student("Donald", 14)
    );

//在onCrateViewHolder中,调用DataBindingUtil.inflate函数,获取rv_item.xml对应的DataBinding布局文件类,并设置给ViewHolder
    @NonNull
    @Override
    public StudentAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        RvItemBinding itemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.rv_item, parent, false);
        return new ViewHolder(itemBinding);
    }

    //在onBindViewHolder函数中,为每个列表条目设置数据
    @Override
    public void onBindViewHolder(@NonNull StudentAdapter.ViewHolder holder, int position) {
        holder.itemBinding.setStudent(list.get(position));
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    //在自定义的ViewHolder类中,维护了rv_item.xml布局文件对应的RvItemBinding数据绑定类实例对象,在构造函数中设置该itemBinding对象
    public class ViewHolder extends RecyclerView.ViewHolder {
        private RvItemBinding itemBinding;

        public ViewHolder(View itemView) {
            super(itemView);
        }

        public ViewHolder(RvItemBinding itemBinding) {
            super(itemBinding.getRoot());
            this.itemBinding = itemBinding;
        }
    }
}

7.4 activity_main.xml

7.5 Activity中

运行效果:

相关推荐
stayong4 小时前
市面主流跨端开发框架对比
前端
庞囧5 小时前
大白话讲 React 原理:Scheduler 任务调度器
前端
东华帝君5 小时前
react 虚拟滚动列表的实现 —— 动态高度
前端
CptW5 小时前
手撕 Promise 一文搞定
前端·面试
温宇飞5 小时前
Web 异步编程
前端
腹黑天蝎座5 小时前
浅谈React19的破坏性更新
前端·react.js
东华帝君5 小时前
react组件常见的性能优化
前端
第七种黄昏5 小时前
【前端高频面试题】深入浏览器渲染原理:从输入 URL 到页面绘制的完整流程解析
前端·面试·职场和发展
angelQ5 小时前
前端fetch手动解析SSE消息体,字符串双引号去除不掉的问题定位
前端·javascript
Huangyi5 小时前
第一节:Flow的基础知识
android·前端·kotlin