UI自动刷新大法:DataBinding数据绑定

之前我们讲了DataBinding在Activity、Fragment、RecyclerView中的基础使用,而那些常规使用方法里,每当绑定的变量发生数据变化时,都需要ViewDataBinding重新设值才会刷新对应UI。而DataBinding通过内部实现的观察者模式来进行自动刷新UI ,这块内容是DataBinding的重要部分。在观察者模式的角度下,DataBinding库,允许我们使用对象、字段,或者集合来进行观察,当其中的一个可观察者数据对象绑定到了视图当中,并且数据对象的属性发生更改变化的时候,视图将会自动更新 。而根据绑定的方式不同,又可分为 单向绑定双向绑定

单向绑定,实现数据变化自动驱动UI刷新,方式有三种:BaseObservable,ObservableField、ObservableCollection。在此之前,先让我们来了解下事件绑定。

前言 事件绑定

为了更好的了解单向绑定和后续的双向绑定,我们先来看下DataBinding中事件绑定的方式。严格来说,事件绑定也是一种变量绑定,只不过设置的绑定不再是单纯的变量,还是回调接口,事件绑定可设置的回调事件有以下:

复制代码
android:onClick
android:onLongClick
android:onTextChanged
android:afterTextChanged
...

对应使用步骤也较为简单:

第一步 声明内部类

在要使用的activity类里新建一个内部类来声明对应的回调方法,这里我们接着用上篇文章中的工程来继续修改。我们在MainActivity中新建一个内部类,里面声明onClick()和afterTextChanged()事件:

复制代码
public class MainActivity extends AppCompatActivity {
    UserInfo userInfo;
    DemoBinding viewDataBinding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        userInfo = new UserInfo("亚历山大", 66);
        viewDataBinding.setUserInfoExample(userInfo);
        viewDataBinding.setUserEventListener(new EventListener());
    }
    public class EventListener{
        public void changedUserName(){
            userInfo.setName("鸭梨山大二世");
            viewDataBinding.setUserInfoExample(userInfo);
        }
        public void changedUserInfo(){
            userInfo.setName("鸭力山大三世");
            userInfo.setAge(81);
            viewDataBinding.setUserInfoExample(userInfo);
        }
    }
}

第二步 修改布局标签内容

同时还要在相应布局文件中的< data>标签里声明此内部类路径,在对应设置此点击事件回调的控件里通过内嵌表达式 @{} 来设置引用。

复制代码
<?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.dbjavatest.MainActivity.EventListener"/>
        <import type="com.example.dbjavatest.bean.UserInfo"/>

        <variable
            name="UserInfoExample"
            type="UserInfo" />

        <variable
            name="UserEventListener"
            type="EventListener" />
    </data>

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

        <TextView
            android:id="@+id/tv_user_first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{()->UserEventListener.changedUserName()}"
            android:text="@{UserInfoExample.name,default=defaultValue}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>

        <TextView
            android:id="@+id/tv_user_second"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(UserInfoExample.age),default=defaultValue}"
            app:layout_constraintTop_toBottomOf="@+id/tv_user_first"
            app:layout_constraintStart_toStartOf="parent"/>

        <Button
            android:id="@+id/btn_change"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@+id/tv_user_second"
            app:layout_constraintStart_toStartOf="parent"
            android:text="改变属性"
            android:onClick="@{()->UserEventListener.changedUserInfo()}"/>

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

在这布局文件中,android:onClick="@{()->UserEventListener.changedUserName()}"属性含义即为点击事件响应方法为指定的UserEventListener中的changedUserName(),运行后,可查看效果如下:

单向数据绑定之Base Observable

众所周知,一个单纯的ViewModel类被更新后,并不会让UI自动更新。Observable存在的目的就是为了数据变更后UI会自动刷新。

在此方面,Observable提供了两个方法:

复制代码
· notifyChange()
· notifyPropertyChanged()

方法一notifyChange()会刷新所有的UI。

方法二notifyPropertyChanged()只会刷新属于它的UI,需要绑定属性(通过注解 @Bindable来绑定)。

使用方式也较为简单:

第一步 修改实体bean类

使实体bean类继承自BaseObservable:

复制代码
public class UserInfo extends BaseObservable {
    public UserInfo(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public void setNameAndAge(String name,int age){
        this.name = name;
        this.age = age;
        notifyChange();
    }

    @Bindable
    public int getAge() {
        return age;
    }

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

    @Bindable
    public String name; //public属性成员可直接在成员变量上方加上@Bindable

    private int age; //private属性成员需要在其get方法上添加@Bindable
}

第二步、修改对应Activity

为了凸显出区别,我们继续沿用上面点击事件中例子的布局,不做修改,但是Activity中代码要修改:

复制代码
public class MainActivity extends AppCompatActivity {
    UserInfo userInfo;
    ActivityMainBinding viewDataBinding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        userInfo = new UserInfo("亚历山大", 66);
        viewDataBinding.setUserInfoExample(userInfo);
        viewDataBinding.setUserEventListener(new EventListener());
    }
    public class EventListener{
        public void changedUserName(){
            userInfo.setName("鸭梨山大二世");
            userInfo.setAge(999);//无效
        }
        public void changedUserInfo(){
            userInfo.setNameAndAge("鸭力山大X世",new Random().nextInt(100));
        }
    }
}

可以看到,相比于普通的点击事件代码中明显少了viewDataBinding.setUserInfoExample...等操作,可能一两个点击事件看不出明显差别,但事件一多,你会发现Activity的代码会省略很多,非常利于代码解耦和整洁性。这也不影响相应效果,运行效果如下:

可见,在点击用户名时,userInfo.setAge(999)执行无效的,因为在原实体bean类中,只设置了改变name属性(notifyPropertyChanged(BR.name)):

所以在点击name属性的时候,只有名字在变化,而点击事件里setNameAndAge()中因为声明了notifyChange();改变所有元素,因此可看到点击按钮时,全局属性跟着改变了(name属性一致固定写死,所以你可能觉得名字没变化)。

二、单项数据绑定之ObservableField

有的时候相应工程里,实体Bean类需要继承其他类,这样就无法使用Observable了。这时有另外一个方案,即ObservableField。PS:ObservableField不需要进行notify操作。

在ObservableField中,官方提供了对基本数据类型的封装,如ObservableInt、ObservableLong、ObservableFloat、ObservableDouble ObservableShort、ObservableBoolean、ObservableByte、ObservableChar 以及 ObservableParcelable 。当然也可通过泛型来申明其他类型,可以说这是官方对Observable中字段的注解和刷新等操作等封装。

其使用方式与Observable还是有点区别的:

第一步 修改实体bean类

复制代码
public class UserInfo{

    public final ObservableField<String> name;

    public final ObservableField<Integer> age;

    public ObservableField<String> getName() {
        return name;
    }

    public ObservableField<Integer> getAge() {
        return age;
    }

    public UserInfo(String name,
                    Integer age){
        this.name=new ObservableField<>(name);
        this.age= new ObservableField<Integer>(age);
    }
}

可见Bean文件在这里取消了继承,对变量进行了public final修饰,重写对应的get()(final修饰的变量无法写set())。其set()属性的方式则有些许不一样。

第二步 修改对应逻辑

在对应需要进行set逻辑的地方,可以通过ObservableField提供的get、set方法,去拿到值和设置值来达到更新UI效果。如下:

复制代码
public class MainActivity extends AppCompatActivity {
    UserInfo userInfo;
    ActivityMainBinding viewDataBinding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        userInfo = new UserInfo("亚历山大", 66);
        viewDataBinding.setUserInfoExample(userInfo);
        viewDataBinding.setUserEventListener(new EventListener());
    }
    public class EventListener{
        public void changedUserName(){
            userInfo.getName().set("鸭梨山大二世");
        }
        public void changedUserInfo(){
            userInfo.getName().set("鸭梨山大一世");
            userInfo.getAge().set(new Random().nextInt(101));
        }
    }
}

对应的效果为:

三、单项数据绑定之ObservableCollection

ObservableCollection中最常用的是ObservableList 和 ObservableMap,即dataBinding 提供的包装类用于替代原生的 List 和 Map。其使用方式与前两者差别也不大。

第一步 修改Collection布局

这里我们不再使用实体bean类,而是dataBinding 包装的ObservableMap和ObservableList元素,因此需要修改< data>标签。如下:

复制代码
<?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>
        <import type="androidx.databinding.ObservableMap"/>
        <import type="androidx.databinding.ObservableList"/>
        <!--注意这里,只能用 "&lt;"和 "&gt;"-->
        <variable
            name="list"
            type="ObservableList&lt;String&gt;"/>
        <variable
            name="map"
            type="ObservableMap&lt;String,Integer&gt;"/>
        <variable
            name="sing"
            type="String"/>
        <variable
            name="num"
            type="int"/>
    </data>

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

        <TextView
            android:id="@+id/tv_user_first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{list[num],default=syt}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>

        <TextView
            android:id="@+id/tv_user_second"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(map[sing]),default=sys}"
            app:layout_constraintTop_toBottomOf="@+id/tv_user_first"
            app:layout_constraintStart_toStartOf="parent"/>

        <Button
            android:id="@+id/btn_change"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@+id/tv_user_second"
            app:layout_constraintStart_toStartOf="parent"
            android:text="改变属性"
            android:onClick="onButtonClick"/>

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

这里的ObservableMap和ObservableList归属的databinding在androidX包里,如果你的工程还没有适配安卓X,最好请尽快适配。最后一个Button里的android:onClick="onButtonClick"可能会让你感到疑惑,但其实这也是dataBinding里的控件的点击事件写法之一。

第二步 修改Activity中对应逻辑

其Activity中代码就如下:

复制代码
public class MainActivity extends AppCompatActivity{
    ActivityMainBinding viewDataBinding;
    private ObservableMap<String, Integer> map;
    ObservableArrayList<String> obList;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        map = new ObservableArrayMap<>();
        map.put("test_num",1 );
        map.put("test_biger_num", 100);
        viewDataBinding.setMap(map);
        obList = new ObservableArrayList<>();
        obList.add("ObservableArrayList");
        obList.add("observablelist");
        obList.add("observablelist more");
        viewDataBinding.setList(obList);
        viewDataBinding.setNum(0);
        viewDataBinding.setSing("test_biger_num");
    }

    public void onButtonClick(View v) {
        map.put("test_biger_num",new Random().nextInt(99));
    }

}

对应效果如下:

当然,设置点击事件还有一种方法引用,直接用 :: 即可,

如: android:onClick="@{listener::onClick}" 就是方法引用绑定!

四、双向数据绑定

双向绑定含义就是 在数据更新时使得View也能更新,而View更新的时候也同时更改数据

此法不适合所有的应用场景,但也有相应的应用场景,比如用户注册登录场景,在输入账号和密码同时,UI刷新同时数据也更新,这里就适合双向绑定。可以说,双向绑定时安卓MVVM架构的基础。

这次简单实例我们用ObservableField方式,先写一个bean实体类:

复制代码
public class DataBean {
    public final ObservableField<String> dataInfo;

    public DataBean(ObservableField<String> dataInfo) {
        this.dataInfo = dataInfo;
    }

    public ObservableField<String> getDataInfo() {
        return dataInfo;
    }
}

对应也要修改布局文件和引入,如下:

复制代码
<?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>
        <import type="com.example.dbjavatest.bean.DataBean"/>
        <variable
            name="dataInfoBean"
            type="DataBean" />
    </data>

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

        <TextView
            android:id="@+id/tv_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="26sp"
            android:textColor="@color/purple_200"
            android:text="@{dataInfoBean.dataInfo}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginLeft="50dp"/>

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="请输入数据"
            android:text="@={dataInfoBean.dataInfo}"
            app:layout_constraintTop_toBottomOf="@+id/tv_data"
            android:textSize="25sp"
            android:layout_marginTop="30dp"
            android:paddingLeft="20dp"/>

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

这里要注意,在EditText中更新数据时会同步到上面的TextView,绑定方式跟单向绑定方式相比,要在内嵌表达式中多用一个"=",即android:text="@={dataInfoBean.dataInfo}"。

对应Activity中代码如下:

复制代码
public class MainActivity extends AppCompatActivity{
    ActivityMainBinding viewDataBinding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        DataBean dataBean = new DataBean(new ObservableField<String>(""));
        viewDataBinding.setDataInfoBean(dataBean);
    }
}

效果也能猜到:

五、List Set Map等数据结构

除了ObservableCollection,DataBinding也支持原生Java数据结构(数组、List、Set和Map)在布局文件中使用,且在布局文件中都可以通过list[index]的形式来获取元素。

官方为了和< variable>标签元素区分开,在声明具有多个泛型的数据类型时,需要使用"& lt;"和 " >"用以区分。如下:

复制代码
<?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.Set" />
        <import type="java.util.Map" />
        <variable
            name="array"
            type="String[]" />
        <variable
            name="list"
            type="List&lt;String&gt;" />
        <variable
            name="map"
            type="Map&lt;String, String&gt;" />
        <variable
            name="set"
            type="Set&lt;String&gt;" />
        <variable
            name="num"
            type="int" />
        <variable
            name="sing"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
        ···
        android:text="@{array[1]}" />
        <TextView
        ···
        android:text="@{list[num]}" />
        <TextView
        ···
        android:text="@{map[sing]}" />
        <TextView
        ···
        android:text='@{map["test"]}' />
    </LinearLayout>
</layout>

六、使用相应类方法

在DataBinding中,使用相应类方法,可现在< data>标签中导入该类不需要写<vari...>标签,然后在布局中像对待一般方法来调用就行。

例如,先写个没啥用的静态类:

复制代码
public class UIUtils {

    public static String showTheDemo(String str) {
        return str.toString();
    }

}

在< data>中引入该类:

复制代码
<import type="com.example.dbjavatest.UIUtils" />

然后在对应控件里调用:

复制代码
<TextView
    android:id="@+id/tv_data"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="26sp"
    android:textColor="@color/purple_200"
    android:text="@{UIUtils.showTheDemo(dataInfoBean.dataInfo)}"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:layout_marginLeft="50dp"/>

PS : DataBinding中布局里的控件通过嵌入表达式不仅可以引用对应方法,也可以使用三元运算符等运算符。

七、include和viewStub

DataBinding也支持include的布局文件,一样通过dataBinding来进行数据绑定,一样需要使用< layout>标签和声明需要使用的变量,然后在主布局中将对应的变量传递给include布局,使两个布局共享的数据变量相同。

例如:view_insert.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">

    <data>
        <import type="com.example.dbjavatest.bean.DataBean"/>
        <variable
            name="dataInfoBean"
            type="DataBean" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="请输入数据"
            android:text="@={dataInfoBean.dataInfo}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:textSize="25sp"
            android:layout_marginTop="30dp"
            android:paddingLeft="20dp"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

对应主布局则直接include,同时通过bind:变量名来将同一变量传过去。你可能发现这里会报错,在布局头文件声明xmlns:bind="http://schemas.android.com/apk/res-auto" 即可。对应布局文件为:

复制代码
<?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"
    xmlns:bind="http://schemas.android.com/apk/res-auto">

    <data>
        <import type="com.example.dbjavatest.bean.DataBean"/>
        <variable
            name="dataInfoBean"
            type="DataBean" />
    </data>

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

        <TextView
            android:id="@+id/tv_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="26sp"
            android:textColor="@color/purple_200"
            android:text="@{dataInfoBean.dataInfo}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginLeft="50dp"/>

        <include
            layout="@layout/view_insert"
            app:layout_constraintTop_toBottomOf="@+id/tv_data"
            bind:dataInfoBean = "@{dataInfoBean}"
            />

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

ViewStub绑定变量和将变量传递给ViewStub的方式与此一致。例如:

复制代码
<ViewStub
    android:id="@+id/view_stub"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout="@layout/view_insert"
    bind:dataInfoBean="@{dataInfoBean}" />

当然,ViewStub 文件一样要使用 layout 等标签进行布局。相应的在Activity中也是通过ViewBinding获取对象实例:

复制代码
View viewStub = viewDataBinding.viewStub.getViewStub().inflate();

通过此实例,可以控制viewStub的可见性。如果在xml中,没用使用bind:dataInfoBean="@{dataInfoBean}",但又想对ViewStub进行数据绑定。则可以在ViewStub 设置 setOnInflateListener回调函数时进行数据绑定,如下:

复制代码
viewDataBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
    @Override
    public void onInflate(ViewStub stub, View inflated) {
        ViewDataBinding viewStubBinding = DataBindingUtil.bind(inflated);
        viewStubBinding.setDataInfoBean(dataBean);
    }
});

对DataBinding的数据绑定就介绍到这里了,如果后续发现有遗漏的要点,会即使补充。另外,如果看这篇文字有点吃力,说明你对DataBinding的基础操作还不熟悉,请翻阅我的上一片文章

相关推荐
李艺为6 小时前
Fake Device Test作假屏幕分辨率分析
android·java
zh_xuan6 小时前
github远程library仓库升级
android·github
峥嵘life6 小时前
Android蓝牙停用绝对音量原理
android
小书房7 小时前
Kotlin的内联函数
java·开发语言·kotlin·inline·内联函数
czlczl200209258 小时前
IN和BETWEEN在索引效能的区别
android·adb
Volunteer Technology8 小时前
ES高级搜索功能
android·大数据·elasticsearch
北京自在科技8 小时前
Find Hub App 小更新
android·ios·安卓·findmy·airtag
lbb 小魔仙8 小时前
2026远程办公软件夏季深度横测:ToDesk、向日葵、网易UU远程全面对比,远控白皮书
android·服务器·网络协议·tcp/ip·postgresql
qcx239 小时前
Warp源码深度解析(二):自研GPU UI框架——WarpUI的ECH模式与渲染管线
人工智能·ui·设计模式·rust
coding_fei9 小时前
AudioServer初始化过程
android