Android DataBinding 從入門到進階(2)
半年多前寫了一篇: Android DataBinding 從入門到進階 的文章,最近發現裡面有些小錯誤,就修改了下文章,並且增添了幾個 DataBinding 的使用例子,希望對你所有幫助
本文是我在學習 Google 的 DataBinding 框架的過程中寫的筆記,會不定時更新,最新一次的更新時間是在 2019-02-27,希望對你有所幫助,也歡迎 Star
專案 GitHub 地址: DataBindingSamples
DataBinding 是谷歌官方釋出的一個框架,顧名思義即為資料繫結,是 MVVM 模式在 Android 上的一種實現,用於降低佈局和邏輯的耦合性,使程式碼邏輯更加清晰。MVVM 相對於 MVP,其實就是將 Presenter 層替換成了 ViewModel 層。DataBinding 能夠省去我們一直以來的 findViewById() 步驟,大量減少 Activity 內的程式碼,資料能夠單向或雙向繫結到 layout 檔案中,有助於防止記憶體洩漏,而且能自動進行空檢測以避免空指標異常
啟用 DataBinding 的方法是在對應 Model 的 build.gradle 檔案里加入以下程式碼,同步後就能引入對 DataBinding 的支援
android { dataBinding { enabled = true } }
一、基礎入門
啟用 DataBinding 後,這裡先來看下如何在佈局檔案中繫結指定的變數
開啟佈局檔案,選中根佈局的 ViewGroup ,按住 Alt + 回車鍵 ,點選 “ Convert to data binding layout ”,就可以生成 DataBinding 需要的佈局規則

<?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> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> </android.support.constraint.ConstraintLayout> </layout>
和原始佈局的區別在於多出了一個 layout 標籤將原佈局包裹了起來, data 標籤用於宣告要用到的變數以及變數型別,要實現 MVVM 的 ViewModel 就需要把資料(Model)與 UI(View)進行繫結, data 標籤的作用就像一個橋樑搭建了 View 和 Model 之間的通道
這裡先來宣告一個 Modle
/** * 作者:leavesC * 時間:2019/2/27 21:36 * 描述: * GitHub:https://github.com/leavesC * Blog:https://www.jianshu.com/u/9df45b87cfdf */ public class User { private String name; private String password; public User(String name, String password) { this.name = name; this.password = password; } ··· }
在 data 標籤裡宣告要使用到的變數名、類的全路徑
<data> <variable name="userInfo" type="leavesc.hello.databindingsamples.model.User" /> </data>
如果 User 型別要多處用到,也可以直接將之 import 進來,這樣就不用每次都指明整個包名路徑了,而 java.lang.*
包中的類會被自動匯入,所以可以直接使用
<data> <import type="leavesc.hello.databindingsamples.model.User"/> <variable name="userInfo" type="User"/> </data>
如果存在 import 的類名相同的情況,可以使用 alias 指定別名
<data> <import type="leavesc.hello.databindingsamples.model.User" /> <import alias="TempUser" type="leavesc.hello.databindingsamples.model2.User" /> <variable name="userInfo" type="User" /> <variable name="tempUserInfo" type="TempUser" /> </data>
這裡聲明瞭一個 User 型別的變數 userInfo ,我們要做的就是使這個變數與兩個 TextView 控制元件掛鉤,通過設定 userInfo 的變數值同時使 TextView 顯示相應的文字
完整的佈局程式碼如下所示
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="leavesc.hello.databindingsamples.model.User" /> <variable name="userInfo" type="User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="20dp" android:orientation="vertical"> <TextView style="@style/titleTextStyle" android:text="單向資料繫結" /> <TextView android:id="@+id/tv_userName" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{userInfo.name}" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{userInfo.password}" /> </LinearLayout> </layout>
通過 @{userInfo.name}
使 TextView 引用到相關的變數,DataBinding 會將之對映到相應的 getter 方法
之後可以在 Activity 中通過 DataBindingUtil
設定佈局檔案,省略原先 Activity 的 setContentView()
方法,併為變數 userInfo 賦值
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMain2Binding binding = DataBindingUtil.setContentView(this, R.layout.activity_main2); User user = new User("leavesC", "123456"); binding.setUserInfo(user); }

由於 @{userInfo.name}
在佈局檔案中並沒有明確的值,所以在預覽檢視中什麼都不會顯示,不便於觀察文字的大小和字型顏色等屬性,此時可以為之設定預設值(文字內容或者是字型大小等屬性都適用),預設值將只在預覽檢視中顯示,且預設值不能包含引號
android:text="@{userInfo.name,default=defaultValue}"
此外,也可以通過 ActivityMain2Binding 直接獲取到指定 ID 的控制元件
activityMain2Binding.tvUserName.setText("leavesC");
每個資料繫結佈局檔案都會生成一個繫結類, ViewDataBinding 的例項名是根據佈局檔名來生成,將之改為首字母大寫的駝峰命名法來命名,並省略佈局檔名包含的下劃線。控制元件的獲取方式類似,但首字母小寫
也可以通過如下方式自定義 ViewDataBinding 的例項名
<data class="CustomBinding"> </data>
此外,在繫結表示式中會根據需要生成一個名為 context
的特殊變數, context
的值是根 View 的 getContext()
方法返回的 Context
物件, context
變數會被具有該名稱的顯式變數宣告所覆蓋
Databinding 同樣是支援在 Fragment 和 RecyclerView 中使用 。例如,可以看 Databinding 在 Fragment 中的使用
@Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentBlankBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_blank, container, false); binding.setHint("Hello"); return binding.getRoot(); }
**以上實現資料繫結的方式,每當繫結的變數發生變化的時候,都需要重新向 ViewDataBinding 傳遞新的變數值才能重新整理 UI 。接下來看如何實現自動重新整理 UI **
二、單向資料繫結
實現資料變化自動驅動 UI 重新整理的方式有三種: BaseObservable
、 ObservableField
、 ObservableCollection
2.1、BaseObservable
一個純淨的 ViewModel 類被更新後,並不會讓 UI 自動更新。而資料繫結後,我們自然會希望資料變更後 UI 會即時重新整理,Observable 就是為此而生的概念
BaseObservable提供了 notifyChange() 和 notifyPropertyChanged() 兩個方法,前者會重新整理所有的值域,後者則只更新對應 BR 的 flag ,該 BR 的生成通過註釋 @Bindable 生成,可以通過 BR notify 特定屬性關聯的檢視
/** * 作者:leavesC * 時間:2019/2/27 21:36 * 描述: * GitHub:https://github.com/leavesC * Blog:https://www.jianshu.com/u/9df45b87cfdf */ public class Goods extends BaseObservable { //如果是 public 修飾符,則可以直接在成員變數上方加上 @Bindable 註解 @Bindable public String name; //如果是 private 修飾符,則在成員變數的 get 方法上新增 @Bindable 註解 private String details; private float price; public Goods(String name, String details, float price) { this.name = name; this.details = details; this.price = price; } public void setName(String name) { this.name = name; //只更新本欄位 notifyPropertyChanged(leavesc.hello.databindingsamples.BR.name); } @Bindable public String getDetails() { return details; } public void setDetails(String details) { this.details = details; //更新所有欄位 notifyChange(); } public float getPrice() { return price; } public void setPrice(float price) { this.price = price; } @Override public String toString() { return "Goods{" + "name='" + name + '\'' + ", details='" + details + '\'' + ", price=" + price + '}'; } }
在 setName() 方法中更新的只是本欄位,而 setDetails() 方法中更新的是所有欄位
新增兩個按鈕用於改變 goods 變數的三個屬性值,由此可以看出兩個 notify 方法的區別。當中涉及的按鈕點選事件繫結,在下面也會講到
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="leavesc.hello.databindingsamples.model.Goods" /> <import type="leavesc.hello.databindingsamples.Main3Activity.GoodsHandler" /> <variable name="goods" type="Goods" /> <variable name="goodsHandler" type="GoodsHandler" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="20dp"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{goods.name}" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{goods.details}" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{String.valueOf(goods.price)}" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{()->goodsHandler.changeGoodsName()}" android:text="改變屬性 name 和 price" android:textAllCaps="false" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{()->goodsHandler.changeGoodsDetails()}" android:text="改變屬性 details 和 price" android:textAllCaps="false" /> </LinearLayout> </layout>
/** * 作者:leavesC * 時間:2019/2/27 21:36 * 描述: * GitHub:https://github.com/leavesC * Blog:https://www.jianshu.com/u/9df45b87cfdf */ public class Main3Activity extends AppCompatActivity { private static final String TAG = "Main3Activity"; private Goods goods; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMain3Binding binding = DataBindingUtil.setContentView(this, R.layout.activity_main3); goods = new Goods("code", "hi", 24); binding.setGoods(goods); binding.setGoodsHandler(new GoodsHandler()); } public class GoodsHandler { public void changeGoodsName() { goods.setName("code" + new Random().nextInt(100)); goods.setPrice(new Random().nextInt(100)); } public void changeGoodsDetails() { goods.setDetails("hi" + new Random().nextInt(100)); goods.setPrice(new Random().nextInt(100)); } } }

實現了 Observable 介面的類允許註冊一個監聽器,當可觀察物件的屬性更改時就會通知這個監聽器,此時就需要用到 OnPropertyChangedCallback
當中 propertyId
就用於標識特定的欄位
goods.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() { @Override public void onPropertyChanged(Observable sender, int propertyId) { if (propertyId == com.leavesc.databinding_demo.BR.name) { Log.e(TAG, "BR.name"); } else if (propertyId == com.leavesc.databinding_demo.BR.details) { Log.e(TAG, "BR.details"); } else if (propertyId == com.leavesc.databinding_demo.BR._all) { Log.e(TAG, "BR._all"); } else { Log.e(TAG, "未知"); } } });
2.2、ObservableField
繼承於 Observable 類相對來說限制有點高,且也需要進行 notify 操作,因此為了簡單起見可以選擇使用 ObservableField 。ObservableField 可以理解為官方對 BaseObservable 中欄位的註解和重新整理等操作的封裝,官方原生提供了對基本資料型別的封裝,例如 ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble 以及 ObservableParcelable ,也可通過 ObservableField 泛型來申明其他型別
/** * 作者:leavesC * 時間:2019/2/27 21:36 * 描述: * GitHub:https://github.com/leavesC * Blog:https://www.jianshu.com/u/9df45b87cfdf */ public class ObservableGoods { private ObservableField<String> name; private ObservableField<String> details; private ObservableFloat price; public ObservableGoods(String name, String details, float price) { this.name = new ObservableField<>(name); this.details = new ObservableField<>(details); this.price = new ObservableFloat(price); } ··· }
對 ObservableGoods 屬性值的改變都會立即觸發 UI 重新整理,概念上與 Observable 區別不大,具體效果可看下面提供的原始碼,這裡不再贅述
2.3、ObservableCollection
DataBinding 也提供了包裝類用於替代原生的 List
和 Map
,分別是 ObservableList
和 ObservableMap
,當其包含的資料發生變化時,繫結的檢視也會隨之進行重新整理
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="android.databinding.ObservableList" /> <import type="android.databinding.ObservableMap" /> <variable name="list" type="ObservableList<String>" /> <variable name="map" type="ObservableMap<String,String>" /> <variable name="index" type="int" /> <variable name="key" type="String" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="20dp" android:text="@{list[index]}" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:padding="20dp" android:text="@{map[key]}" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="onClick" android:text="改變資料" /> </LinearLayout> </layout>
private ObservableMap<String, String> map; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMain5Binding binding = DataBindingUtil.setContentView(this, R.layout.activity_main5); map = new ObservableArrayMap<>(); map.put("name", "leavesC"); map.put("age", "24"); binding.setMap(map); ObservableList<String> list = new ObservableArrayList<>(); list.add("Ye"); list.add("leavesC"); binding.setList(list); binding.setIndex(0); binding.setKey("name"); } public void onClick(View view) { map.put("name", "leavesC,hi" + new Random().nextInt(100)); }

三、雙向資料繫結
雙向繫結的意思即為當資料改變時同時使檢視重新整理,而檢視改變時也可以同時改變資料
看以下例子,當 EditText 的輸入內容改變時,會同時同步到變數 goods
,繫結變數的方式比單向繫結多了一個等號: android:text="@={goods.name}"
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="leavesc.hello.databindingsamples.model.ObservableGoods" /> <variable name="goods" type="ObservableGoods" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="20dp" android:orientation="vertical"> <TextView style="@style/titleTextStyle" android:layout_marginTop="10dp" android:text="雙向資料繫結" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{goods.name}" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@={goods.name}" /> </LinearLayout> </layout>
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMain2Binding binding = DataBindingUtil.setContentView(this, R.layout.activity_main2); ObservableGoods goods = new ObservableGoods("code", "hi", 23); binding.setGoods(goods); }

四、事件繫結
嚴格意義上來說,事件繫結也是一種變數繫結,只不過設定的變數是回撥介面而已
事件繫結可用於以下多種回撥事件
- android:onClick
- android:onLongClick
- android:afterTextChanged
- android:onTextChanged
- ...
在 Activity 內部新建一個 UserPresenter 類來宣告 onClick() 和 afterTextChanged() 事件相應的回撥方法
public class UserPresenter { public void onUserNameClick(User user) { Toast.makeText(Main6Activity.this, "使用者名稱:" + user.getName(), Toast.LENGTH_SHORT).show(); } public void afterTextChanged(Editable s) { user.setName(s.toString()); binding.setUserInfo(user); } public void afterUserPasswordChanged(Editable s) { user.setPassword(s.toString()); binding.setUserInfo(user); } }
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="leavesc.hello.databindingsamples.model.User" /> <import type="leavesc.hello.databindingsamples.Main6Activity.UserPresenter" /> <import type="leavesc.hello.databindingsamples.StringUtils" /> <variable name="userInfo" type="User" /> <variable name="userPresenter" type="UserPresenter" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="20dp" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{()->userPresenter.onUserNameClick(userInfo)}" android:text="@{StringUtils.toUpperCase(userInfo.name)}" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{userInfo.password}" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:afterTextChanged="@{userPresenter.afterTextChanged}" android:hint="使用者名稱" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:afterTextChanged="@{userPresenter.afterUserPasswordChanged}" android:hint="密碼" /> </LinearLayout> </layout>
方法引用的方式與呼叫函式的方式類似,既可以選擇保持事件回撥方法的簽名一致: @{userPresenter.afterTextChanged} ,此時方法名可以不一樣,但方法引數和返回值必須和原始的回撥函式保持一致。也可以引用不遵循預設簽名的函式: @{()->userPresenter.onUserNameClick(userInfo)} ,這裡用到了 Lambda 表示式,這樣就可以不遵循預設的方法簽名,將 userInfo
物件直接傳回點選方法中。此外,也可以使用方法引用 :: 的形式來進行事件繫結

五、使用類方法
首先定義一個靜態方法
public class StringUtils { public static String toUpperCase(String str) { return str.toUpperCase(); } }
在 data 標籤中匯入該工具類
<import type="leavesc.hello.databindingsamples.StringUtils" />
然後就可以像對待一般的函式一樣來呼叫了
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{()->userPresenter.onUserNameClick(userInfo)}" android:text="@{StringUtils.toUpperCase(userInfo.name)}" />
六、運算子
6.1、基礎運算子
DataBinding 支援在佈局檔案中使用以下運算子、表示式和關鍵字
- 算術 + - / * %
- 字串合併 +
- 邏輯 && ||
- 二元 & | ^
- 一元 + - ! ~
- 移位 >> >>> <<
- 比較 == > < >= <=
- Instanceof
- Grouping ()
- character, String, numeric, null
- Cast
- 方法呼叫
- Field 訪問
- Array 訪問 []
- 三元 ?:
目前不支援以下操作
- this
- super
- new
- 顯示泛型呼叫
此外,DataBinding 還支援以下幾種形式的呼叫
6.2、Null Coalescing
空合併運算子 ?? 會取第一個不為 null 的值作為返回值
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{user.name ?? user.password}" />
等價於
android:text="@{user.name != null ? user.name : user.password}"
6.3、屬性控制
可以通過變數值來控制 View 的屬性
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="可見性變化" android:visibility="@{user.male? View.VISIBLE : View.GONE}" />
6.4、避免空指標異常
DataBinding 也會自動幫助我們避免空指標異常
例如,如果 "@{userInfo.password}" 中 userInfo 為 null 的話, userInfo.password 會被賦值為預設值 null ,而不會丟擲空指標異常
七、include 和 viewStub
7.1、include
對於 include 的佈局檔案,一樣是支援通過 dataBinding 來進行資料繫結,此時一樣需要在待 include 的佈局中依然使用 layout 標籤,宣告需要使用到的變數
view_include.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="leavesc.hello.databindingsamples.model.User" /> <variable name="userInfo" type="User" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#acc"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="20dp" android:text="@{userInfo.name}" /> </android.support.constraint.ConstraintLayout> </layout>
在主佈局檔案中將相應的變數傳遞給 include 佈局,從而使兩個佈局檔案之間共享同一個變數
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="leavesc.hello.databindingsamples.model.User" /> <variable name="userInfo" type="User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <include layout="@layout/view_include" bind:userInfo="@{userInfo}" /> </LinearLayout> </layout>
7.2、viewStub
dataBinding 一樣支援 ViewStub 佈局
在佈局檔案中引用 viewStub 佈局
<ViewStub android:id="@+id/view_stub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/view_stub"/>
獲取到 ViewStub 物件,由此就可以來控制 ViewStub 的可見性
ActivityMain7Binding binding = DataBindingUtil.setContentView(this, R.layout.activity_main7); View view = binding.viewStub.getViewStub().inflate();
如果需要為 ViewStub 繫結變數值,則 ViewStub 檔案一樣要使用 layout 標籤進行佈局,主佈局檔案使用自定義的 bind 名稱空間將變數傳遞給 ViewStub
<ViewStub android:id="@+id/view_stub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/view_stub" bind:userInfo="@{userInfo}" />
如果在 xml 中沒有使用 bind:userInfo="@{userInf}"
對 ViewStub 進行資料繫結,則可以等到當 ViewStub Inflate 時再繫結變數,此時需要為 ViewStub 設定 setOnInflateListener
回撥函式,在回撥函式中進行資料繫結
binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() { @Override public void onInflate(ViewStub stub, View inflated) { //如果在 xml 中沒有使用 bind:userInfo="@{userInf}" 對 viewStub 進行資料繫結 //那麼可以在此處進行手動繫結 ViewStubBinding viewStubBinding = DataBindingUtil.bind(inflated); viewStubBinding.setUserInfo(user); Log.e(TAG, "onInflate"); } });
八、BindingAdapter
dataBinding 提供了 BindingAdapter 這個註解用於支援自定義屬性,或者是修改原有屬性。註解值可以是已有的 xml 屬性,例如 android:src
、 android:text
等,也可以自定義屬性然後在 xml 中使用
例如,對於一個 ImageView ,我們希望在某個變數值發生變化時,可以動態改變顯示的圖片,此時就可以通過 BindingAdapter 來實現
需要先定義一個靜態方法,為之新增 BindingAdapter 註解,註解值是為 ImageView 控制元件自定義的屬性名,而該靜態方法的兩個引數可以這樣來理解:當 ImageView 控制元件的 url 屬性值發生變化時,dataBinding 就會將 ImageView 例項以及新的 url 值傳遞給 loadImage() 方法,從而可以在此動態改變 ImageView 的相關屬性
@BindingAdapter({"url"}) public static void loadImage(ImageView view, String url) { Log.e(TAG, "loadImage url : " + url); }
在 xml 檔案中關聯變數值,當中,bind 這個名稱可以自定義
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="leavesc.hello.databindingsamples.model.Image" /> <variable name="image" type="Image" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher_background" bind:url="@{image.url}" /> </android.support.constraint.ConstraintLayout> </layout>
BindingAdapter 更為強大的一點是可以覆蓋 Android 原先的控制元件屬性。例如,可以設定每一個 Button 的文字都要加上字尾:“-Button”
@BindingAdapter("android:text") public static void setText(Button view, String text) { view.setText(text + "-Button"); }
<Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{()->handler.onClick(image)}" android:text='@{"改變圖片Url"}'/>
這樣,整個工程中使用到了 "android:text" 這個屬性的控制元件,其顯示的文字就會多出一個字尾

九、BindingConversion
dataBinding 還支援對資料進行轉換,或者進行型別轉換
與 BindingAdapter 類似,以下方法會將佈局檔案中所有以 @{String}
方式引用到的 String
型別變數加上字尾 -conversionString
@BindingConversion public static String conversionString(String text) { return text + "-conversionString"; }
xml 檔案
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text='@{"xxx"}' android:textAllCaps="false"/>

可以看到,對於 Button 來說,BindingAdapter 和 BindingConversion 同時生效了,而 BindingConversion 的優先順序要高些
此外,BindingConversion 也可以用於轉換屬性值的型別
看以下佈局,此處在向 background
和 textColor
兩個屬性賦值時,直接就使用了字串,按正常情況來說這自然是會報錯的,但有了 BindingConversion 後就可以自動將字串型別的值轉為需要的 Drawable
和 Color
了
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background='@{"紅色"}' android:padding="20dp" android:text="紅色背景藍色字" android:textColor='@{"藍色"}'/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:background='@{"藍色"}' android:padding="20dp" android:text="藍色背景紅色字" android:textColor='@{"紅色"}'/>
@BindingConversion public static Drawable convertStringToDrawable(String str) { if (str.equals("紅色")) { return new ColorDrawable(Color.parseColor("#FF4081")); } if (str.equals("藍色")) { return new ColorDrawable(Color.parseColor("#3F51B5")); } return new ColorDrawable(Color.parseColor("#344567")); } @BindingConversion public static int convertStringToColor(String str) { if (str.equals("紅色")) { return Color.parseColor("#FF4081"); } if (str.equals("藍色")) { return Color.parseColor("#3F51B5"); } return Color.parseColor("#344567"); }

十、Array、List、Set、Map ...
dataBinding 也支援在佈局檔案中使用 陣列、Lsit、Set 和 Map ,且在佈局檔案中都可以通過 list[index]
的形式來獲取元素
而為了和 variable 標籤的尖括號區分開,在宣告 Lsit<String> 之類的資料型別時,需要使用尖括號的轉義字元
<?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.Map" /> <import type="java.util.Set" /> <import type="android.util.SparseArray" /> <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="sparse" type="SparseArray<String>" /> <variable name="index" type="int" /> <variable name="key" type="String" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".Main7Activity"> <TextView ··· android:text="@{array[1]}" /> <TextView ··· android:text="@{sparse[index]}" /> <TextView ··· android:text="@{list[index]}" /> <TextView ··· android:text="@{map[key]}" /> <TextView ··· android:text='@{map["leavesC"]}' /> <TextView ··· android:text='@{set.contains("xxx")?"xxx":key}' /> </LinearLayout> </layout>
十一、資源引用
dataBinding 支援對尺寸和字串這類資源的訪問
dimens.xml
<dimen name="paddingBig">190dp</dimen> <dimen name="paddingSmall">150dp</dimen>
strings.xml
<string name="format">%s is %s</string>
<data> <variable name="flag" type="boolean" /> </data> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="@{flag ? @dimen/paddingBig:@dimen/paddingSmall}" android:text='@{@string/format("leavesC", "Ye")}' android:textAllCaps="false" />
十二、與 RecyclerView 搭配使用
dataBinding 與 RecyclerView 搭配使用的話可以讓程式碼更加簡潔明瞭
先宣告需要的 item 佈局檔案
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="leavesc.hello.databindingsamples.model.User" /> <variable name="user" type="User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingLeft="10dp"> <TextView android:id="@+id/tvName" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="8dp" android:text="@{user.name}" /> <TextView android:id="@+id/tvPassword" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="8dp" android:text="@{user.password}" /> <View android:layout_width="match_parent" android:layout_height="1dp" android:background="#c6cdd4" /> </LinearLayout> </layout>
對應的 RecyclerView.Adapter
/** * 作者:leavesC * 時間:2019/2/27 21:36 * 描述: * GitHub:https://github.com/leavesC * Blog:https://www.jianshu.com/u/9df45b87cfdf */ public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterHolder> { private List<User> userList; public UserAdapter(List<User> userList) { this.userList = userList; } @NonNull @Override public UserAdapterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { ItemUserBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_user, parent, false); return new UserAdapterHolder(binding); } @Override public void onBindViewHolder(@NonNull UserAdapterHolder holder, int position) { holder.getBinding().setUser(userList.get(position)); } @Override public int getItemCount() { if (userList == null) { return 0; } return userList.size(); } class UserAdapterHolder extends RecyclerView.ViewHolder { private ItemUserBinding binding; UserAdapterHolder(ItemUserBinding binding) { super(binding.getRoot()); this.binding = binding; } public ItemUserBinding getBinding() { return binding; } } }
十三、RecyclerView Adapter 高效率重新整理
前文講到了 ObservableList ,此處就可以通過 ObservableList 的實現類 ObservableArrayList 來實現 RecyclerView Adapter 的高效重新整理 ,而不是每次都是直接 notifyDataSetChanged
可以先看下 ObservableArrayList 的原始碼,可以發現在每次 增刪改資料 時,都會觸發到 ListChangeRegistry 內的 OnListChangedCallback 回撥,且 OnListChangedCallback 是把每次改動到的資料位置都給透傳到外部,我們可以通過這些資訊來只重新整理 Adapter 的特定位置,從而實現高效重新整理,並且獲得一些動畫效果
public class ObservableArrayList<T> extends ArrayList<T> implements ObservableList<T> { private transient ListChangeRegistry mListeners = new ListChangeRegistry(); ··· @Override public boolean add(T object) { super.add(object); notifyAdd(size() - 1, 1); return true; } @Override public void clear() { int oldSize = size(); super.clear(); if (oldSize != 0) { notifyRemove(0, oldSize); } } @Override public T remove(int index) { T val = super.remove(index); notifyRemove(index, 1); return val; } @Override public T set(int index, T object) { T val = super.set(index, object); if (mListeners != null) { mListeners.notifyChanged(this, index, 1); } return val; } private void notifyAdd(int start, int count) { if (mListeners != null) { mListeners.notifyInserted(this, start, count); } } private void notifyRemove(int start, int count) { if (mListeners != null) { mListeners.notifyRemoved(this, start, count); } } ··· }
此處通過 DynamicChangeCallback 來實現對 Adapter 的重新整理操作
/** * 作者:leavesC * 時間:2019/2/27 21:36 * 描述: * GitHub:https://github.com/leavesC * Blog:https://www.jianshu.com/u/9df45b87cfdf */ public class DynamicChangeCallback<T> extends ObservableList.OnListChangedCallback<ObservableList<T>> { private RecyclerView.Adapter adapter; public DynamicChangeCallback(RecyclerView.Adapter adapter) { this.adapter = adapter; } @Override public void onChanged(ObservableList<T> sender) { adapter.notifyDataSetChanged(); } @Override public void onItemRangeChanged(ObservableList<T> sender, int positionStart, int itemCount) { adapter.notifyItemRangeChanged(positionStart, itemCount); } @Override public void onItemRangeInserted(ObservableList<T> sender, int positionStart, int itemCount) { adapter.notifyItemRangeInserted(positionStart, itemCount); } @Override public void onItemRangeMoved(ObservableList<T> sender, int fromPosition, int toPosition, int itemCount) { adapter.notifyItemRangeRemoved(fromPosition, itemCount); adapter.notifyItemRangeInserted(toPosition, itemCount); } @Override public void onItemRangeRemoved(ObservableList<T> sender, int positionStart, int itemCount) { adapter.notifyItemRangeRemoved(positionStart, itemCount); } }
通過幾個按鈕來分別測試 Adapter 的資料重新整理情況
public class Main13Activity extends AppCompatActivity { private ObservableArrayList<User> userObservableArrayList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main13); RecyclerView rvList = findViewById(R.id.rvList); rvList.setLayoutManager(new LinearLayoutManager(this)); initData(); UserAdapter userAdapter = new UserAdapter(userObservableArrayList); userAdapter.notifyDataSetChanged(); userObservableArrayList.addOnListChangedCallback(new DynamicChangeCallback(userAdapter)); rvList.setAdapter(userAdapter); } private void initData() { userObservableArrayList = new ObservableArrayList<>(); for (int i = 0; i < 20; i++) { User user = new User("user_" + i, String.valueOf(new Random().nextInt() * 4)); userObservableArrayList.add(user); } } public void addItem(View view) { if (userObservableArrayList.size() >= 3) { User user = new User("user_" + 100, String.valueOf(new Random().nextInt() * 4)); userObservableArrayList.add(1, user); } } public void addItemList(View view) { if (userObservableArrayList.size() >= 3) { List<User> userList = new ArrayList<>(); for (int i = 0; i < 3; i++) { User user = new User("user_" + 100, String.valueOf(new Random().nextInt() * 4)); userList.add(user); } userObservableArrayList.addAll(1, userList); } } public void removeItem(View view) { if (userObservableArrayList.size() >= 3) { userObservableArrayList.remove(1); } } public void updateItem(View view) { if (userObservableArrayList.size() >= 3) { User user = userObservableArrayList.get(1); user.setName("user_" + new Random().nextInt()); userObservableArrayList.set(1, user); } } }
