之前我们讲了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"/>
<!--注意这里,只能用 "<"和 ">"-->
<variable
name="list"
type="ObservableList<String>"/>
<variable
name="map"
type="ObservableMap<String,Integer>"/>
<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<String>" />
<variable
name="map"
type="Map<String, String>" />
<variable
name="set"
type="Set<String>" />
<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的基础操作还不熟悉,请翻阅我的上一片文章。