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>標籤裡宣告此內部類路徑,在對應設定此點選事件回撥的控制元件裡通過內嵌表示式 @{} 來設定引用。 ```
<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(),執行後,可檢視效果如下:
一 、 單向資料繫結之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>標籤。如下: ```
<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>
``` 這裡的ObservableMap和ObservableList歸屬的databinding在androidX包裡,如果你的工程還沒有適配安卓X,最好請儘快適配。最後一個Button裡的android:onClick="onButtonClick"可能會讓你感到疑惑,但其實這也是dataBinding裡的控制元件的點選事件寫法之一。
第二步 、 修改Activity中對應邏輯
其Activity中程式碼就如下:
```
public class MainActivity extends AppCompatActivity{
ActivityMainBinding viewDataBinding;
private ObservableMap
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
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);
}
}
效果也能猜到:
三、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<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>
```
四、使用相應類方法
在DataBinding中,使用相應類方法,可現在< data>標籤中匯入該類不需要寫
例如,先寫個沒啥用的靜態類: ``` public class UIUtils {
public static String showTheDemo(String str) {
return str.toString();
}
}
在< data>中引入該類:
然後在對應控制元件裡呼叫:
五、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的基礎操作還不熟悉,請翻閱我的上一片文章。
- 一文講完Jetpack常用修飾符
- JetpackCompose中的Dialog、AlertDialog
- Activity互動問題,你確定都知道?
- Kotlin中的內建函式-apply、let
- Compose自定義動畫API指南
- Android EditText關於imeOptions的設定和響應
- Jetpack 之Glance Compose實現一個小元件
- Compose高級別API動畫指南
- Android錄音功能的實現及踩坑記錄
- LayoutInflater原始碼解析及常見相關報錯分析
- UI自動重新整理大法:DataBinding資料繫結
- DataBinding簡易入門
- 還在用findViewById,不來了解下其它方式?