UI自動重新整理大法:DataBinding資料繫結

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 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>標籤裡宣告此內部類路徑,在對應設定此點選事件回撥的控制元件裡通過內嵌表示式 @{} 來設定引用。 ```

<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>

``` 在這佈局檔案中,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>標籤。如下: ```

<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>

``` 這裡的ObservableMap和ObservableList歸屬的databinding在androidX包裡,如果你的工程還沒有適配安卓X,最好請儘快適配。最後一個Button裡的android:onClick="onButtonClick"可能會讓你感到疑惑,但其實這也是dataBinding裡的控制元件的點選事件寫法之一。

第二步 修改Activity中對應邏輯

其Activity中程式碼就如下: ``` public class MainActivity extends AppCompatActivity{ ActivityMainBinding viewDataBinding; private ObservableMap map; ObservableArrayList 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 dataInfo;

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

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

} 對應也要修改佈局檔案和引入<data>,如下:

<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>

``` 這裡要注意,在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;"和 " >"用以區分。如下: ```

<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>

```

四、使用相應類方法

在DataBinding中,使用相應類方法,可現在< data>標籤中匯入該類不需要寫標籤,然後在佈局中像對待一般方法來呼叫就行。

例如,先寫個沒啥用的靜態類: ``` public class UIUtils {

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

} 在< data>中引入該類: 然後在對應控制元件裡呼叫: ``` PS : DataBinding中佈局裡的控制元件通過嵌入表示式不僅可以引用對應方法,也可以使用三元運算子等運算子。

五、include和viewStub

DataBinding也支援include的佈局檔案,一樣通過dataBinding來進行資料繫結,一樣需要使用< layout>標籤和宣告需要使用的變數,然後在主佈局中將對應的變數傳遞給include佈局,使兩個佈局共享的資料變數相同。

例如:view_insert.xml中 ```

<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>

對應主佈局則直接include,同時通過bind:變數名來將同一變數傳過去。你可能發現這裡會報錯,在佈局標頭檔案宣告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>

ViewStub繫結變數和將變數傳遞給ViewStub的方式與此一致。例如: 當然,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的基礎操作還不熟悉,請翻閱我的上一片文章