Android MVVM 初探之 DataBinding 双向绑定

时间:2022-08-02 09:27:16

摘要
在google 刚推出 DataBinding 时是只支持单向绑定的,也即数据可以显示到 View 上来,而 View 上进行更新却不能同步到 数据上去。而现在则可以。
除数据外,在响应事件上也比原来更加方便,快捷。

属性双向绑定示例
效果:
Android MVVM 初探之 DataBinding 双向绑定
代码: Setting.java

public class Setting extends BaseObservable{
    private boolean voiceOn = false;
    private boolean vibrateOn = false;
    private boolean cacheEnable = false;
    private int cacheSize = 0;

    @Override
    public void notifyPropertyChanged(int fieldId) {
        super.notifyPropertyChanged(fieldId);
        if (fieldId != BR.description){
            notifyPropertyChanged(BR.description);
        }
    }

    @Bindable
    public void setCacheEnable(boolean cacheEnable) {
        this.cacheEnable = cacheEnable;
        notifyPropertyChanged(BR.cacheEnable);
    }

    @Bindable
    public void setCacheSize(int cacheSize) {
        this.cacheSize = cacheSize;
        notifyPropertyChanged(BR.cacheSize);
    }

    @Bindable
    public void setVibrateOn(boolean vibrateOn) {
        this.vibrateOn = vibrateOn;
        notifyPropertyChanged(BR.vibrateOn);
    }

    @Bindable
    public void setVoiceOn(boolean voiceOn) {
        this.voiceOn = voiceOn;
        notifyPropertyChanged(BR.voiceOn);
    }

    @Bindable
    public int getCacheSize() {
        return cacheSize;
    }

    @Bindable
    public boolean isCacheEnable() {
        return cacheEnable;
    }

    @Bindable
    public boolean isVibrateOn() {
        return vibrateOn;
    }

    @Bindable
    public boolean isVoiceOn() {
        return voiceOn;
    }

    @Bindable
    public String getDescription(){
        return this.toString();
    }

    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(this.getClass().getSimpleName());
        stringBuilder.append(": ");
        stringBuilder.append("\nvibrateOn:");
        stringBuilder.append(vibrateOn);
        stringBuilder.append("\nvoiceOn:");
        stringBuilder.append(voiceOn);
        stringBuilder.append("\ncacheEnable: ");
        stringBuilder.append(cacheEnable);
        if (cacheEnable){
            stringBuilder.append("\ncacheSize: ");
            stringBuilder.append(cacheSize);
        }
        return stringBuilder.toString();
    }

}

对应的xml 文件: activity_syn.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="android.view.View" />
        <variable  name="setting" type="me.leo.mvvm.bean.Setting" />
    </data>

    <GridLayout  android:layout_width="match_parent" android:layout_height="match_parent" android:columnCount="2">

        <TextView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/syn_vibrate" />

        <ToggleButton  android:id="@+id/syn_vibrate_on" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" android:checked="@={setting.vibrateOn}" android:background="@drawable/toggle_btn" android:textOff="" android:textOn=""/>

        <TextView android:text="@string/syn_voice" android:layout_height="wrap_content" android:layout_width="wrap_content"/>

        <ToggleButton  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" android:checked="@={setting.voiceOn}" android:background="@drawable/toggle_btn" android:textOff="" android:textOn=""/>

        <TextView android:text="@string/syn_cache" android:layout_height="wrap_content" android:layout_width="wrap_content"/>

        <ToggleButton  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" android:checked="@={setting.cacheEnable}" android:background="@drawable/toggle_btn" android:textOff="" android:textOn=""/>

        <TextView  android:text="@string/syn_cache_size" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{setting.cacheEnable ? View.VISIBLE: View.GONE}" android:layout_columnSpan="2"/>

        <SeekBar  android:layout_height="wrap_content" android:layout_width="match_parent" android:visibility="@{setting.cacheEnable ? View.VISIBLE: View.GONE}" android:progress="@={setting.cacheSize}" android:max="1000" android:layout_columnSpan="2"/>

        <TextView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/syn_setting" android:layout_columnSpan="2"/>

        <TextView  android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{setting.description}" android:layout_columnSpan="2"/>
    </GridLayout>
</layout>

首先观察怎么用的,这里 Setting 继承了 BaseObservable 通过调用 notifyPropertyChanged(int fieldId); 来通知 View 更新数据。在需要使用的 field 的对应的 getter 和 setter 上加上注解 @Bindable 从而实现与 xml 的绑定。
而 xml 文件从原来的 @{variable.value} 变成了 @={variable.value} 用以响应界面上对数据的更改。
两相结合,在 activity 中只要将两者按类似下面这样绑定,即可实现 View 实时相应界面其它部分的变化。同时绑定时的 Setting 值也同时已经更新。

public class SynActivity extends AppCompatActivity{

    private Setting setting = new Setting();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivitySynBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_syn);
        binding.setSetting(setting);
    }
}

还有另外一种方式实现绑定,即使用 ObservableField<T> 这样的方式,可以有类似下面这样的代码:

public class Setting extends BaseObservable{
    public final ObservableField<String> description = new ObservableField<>();
    public final ObservableBoolean voiceOn = new ObservableBoolean();
}

查看 ObservableField 的定义

public class ObservableField<T> extends BaseObservable implements Serializable public class BaseObservable implements Observable 

可以发现还是上面的那一套。
那么下面的这一套要响应事件也就可以通过 BaseObservable

    public void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        synchronized (this) {
            if (mCallbacks == null) {
                mCallbacks = new PropertyChangeRegistry();
            }
        }
        mCallbacks.add(callback);
    }

将需要处理的事件添加进去即可。如上面的这种即可在构造函数中对需要关注的 Field 添加 callback ,当其值改变的时候,通知 description 值也发生了变化,需要注意的是,通过这种方式使用的 Field 必须要声明为 public, 原因应该比较容易想到。

事件绑定
在未使用 databinding 的情况下 xml 也是支持部分事件绑定的,如:

<LinearLayout  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" tools:context=".MainActivity">

    <Button  android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/to_list" android:onClick="toList"/>
</LinearLayout>

然后在对应的 MainActivity 中加上这样的一个函数

    public void toList(View view){
        // 你要做的事情
    }

在单击这个 Button 的时候就会调用这样的一个函数。但这样的操作是有限制的, 首先,我们需要声明 tools名称空间,然后设定 Context 是 MainActivity。其次,定义在 MainActivity 中的函数形式必须与 OnClickListener 的参数形式完全一致,否则不可调用。
而 DataBinding 则不一定要这样。
DataBinding 的事件处理有两种方式:

  • 绑定 Method,如同上面的那样,其函数入参必须要和原来的一致
  • 绑定 Listener,没有Method 这样的要求,这里会使用一个 Lambda 表达式进行实现。

以上两者另外的一个区别在于事件绑定时间, Method 在数据设定时绑定,而 Listener 在事件触发的时候
两个简单的例子,来源于 谷歌 DataBinding 介绍
首先看 Method Binding

public class MyHandlers {
    public void onClickFriend(View view) { ... }
}

对应的 xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout  android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}" android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

而对应的 Listener Bindings 则如下

public class Presenter {
    public void onSaveClick(Task task){}
}

对应的 xml

<?xml version="1.0" encoding="utf-8"?>
  <layout xmlns:android="http://schemas.android.com/apk/res/android">
      <data>
          <variable name="task" type="com.android.example.Task" />
          <variable name="presenter" type="com.android.example.Presenter" />
      </data>
      <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
          <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="@{() -> presenter.onSaveClick(task)}" />
      </LinearLayout>
  </layout>

这里的例子中 Method Binding 在单击的时候,直接调用了 Handler 的 OnClickFriend 方法,传参为 andriod.view.View 而 Listener Binding 对应的则是通过 Lambda 封装了一层,在封装的那一层里面通过 presenter 调用了对应的 task 任务。
而且 Listener Binding 可以改造成这样的:

public class Presenter {
    public void onSaveClick(View view, Task task){}
}
<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}" />

甚至对于 CheckBox 这样的还有:

public class Presenter {
    public void onCompletedChanged(Task task, boolean completed){}
}
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content" android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

注意
ListenerBinding 和 Method Binding 的返回类型必须要和 View 对应的事件类型所需要的返回值一致

小结
以上是关于数据双向绑定主要需要了解的一些内容,具体细节还有很多:

  1. 在 xml 中进行数据操作,可以进行的运算
  2. 设定自定义 View 时需要使用的方法
  3. 自定义属性的方法

但不适合记录在这里了,数据绑定内容基本到此结束,但如何真正实现一个 mvvm 的应用,这还是一个问题,后面继续探究。

参考资料
本篇文章参考资料完全来源于 Google