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

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

之前我们讲了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(),运行后,可查看效果如下:

DataBinding点击事件.gif

单向数据绑定之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的代码会省略很多,非常利于代码解耦和整洁性。这也不影响相应效果,运行效果如下:

DataBinding中的Observable点击事件.gif

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

image.gif 所以在点击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));
        }
    }
}
复制代码

对应的效果为:

DataBinding-Field点击事件.gif

三、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));
    }

}
复制代码

对应效果如下:

DataBinding_ObservableCollection点击事件.gif

当然,设置点击事件还有一种方法引用,直接用 :: 即可, 如: 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);
    }
}
复制代码

效果也能猜到:

DataBinding双向绑定.gif

三、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="schemas.android.com/apk/res-aut…" 即可。对应布局文件为:

<?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的基础操作还不熟悉,请翻阅我的上一片文章

猜你喜欢

转载自juejin.im/post/7102832927884443684