數據綁定庫
概覽
數據綁定庫是一種支持庫,藉助該庫,您可以使用聲明性格式(而非程序化地)將佈局中的界面組件綁定到應用中的數據源。
1. 開始使用
瞭解如何準備開發環境以使用數據綁定庫,包括在 Android Studio 中支持數據綁定代碼。
2. 佈局和綁定表達式
藉助表達式語言,您可以編寫將變量關聯到佈局中的視圖的表達式。數據綁定庫會自動生成將佈局中的視圖與您的數據對象綁定所需的類。該庫提供了可在佈局中使用的導入、變量和包含等功能。
該庫的這些功能可與您的現有佈局無縫地共存。例如,可以在表達式中使用的綁定變量在 data 元素(界面佈局根元素的同級)內定義。這兩個元素都封裝在__layout__ 標記中,如以下示例所示
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewmodel"
type="com.myapp.data.ViewModel" />
</data>
<ConstraintLayout... /> <!-- UI layout's root element -->
</layout>
3. 使用可觀察的數據對象
數據綁定庫提供了可讓您輕鬆地觀察數據更改情況的類和方法。您不必操心在底層數據源發生更改時刷新界面。您可以將變量或其屬性設爲可觀察。藉助該庫,您可以將對象、字段或集合設爲可觀察。
4. 生成的綁定類
數據綁定庫可以生成用於訪問佈局變量和視圖的綁定類。此頁面展示瞭如何使用和自定義所生成的綁定類。
5. 綁定適配器
每一個佈局表達式都有一個對應的綁定適配器,要求必須進行框架調用來設置相應的屬性或監聽器。
例如,綁定適配器負責調用 setText() 方法來設置文本屬性,或者調用 setOnClickListener() 方法向點擊事件添加監聽器。最常擁的綁定適配器(例如針對本頁面的示例中使用的 android:text 屬性)可供您在 android.databinding.adapters 軟件包中使用。如需常用綁定適配器列表,請參閱適配器。您也可以按照以下示例所示創建自定義適配器:
@BindingAdapter("app:goneUnless")
fun goneUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.GONE
}
將佈局視圖綁定到架構組件
Android 支持庫包含架構組件,您可以使用這些組件設計穩健、可測試且易維護的應用。您可以將架構組件與數據綁定庫一起使用,以進一步簡化界面開發。
雙向數據綁定
數據綁定庫支持雙向數據綁定。此類綁定使用的表示法支持以下操作:
- 接收對屬性的數據更改
- 監聽用戶對此屬性的更新
使用入門
在app的build.gradle文件中添加dataBinding元素
android {
···
dataBinding {
enabled = true
}
}
佈局和綁定表達式
數據綁定佈局文件略有不同,以根標記 layout
開頭,後跟 data
元素和 view
根元素。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<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}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
數據對象
data class User(val firstName: String, val lastName: String)
綁定數據
系統會爲每個佈局文件生成一個綁定類。默認情況下,類名稱基於佈局文件的名稱,它會轉換爲 Pascal 大小寫形式(大駝峯命名方式)並__在末尾添加 Binding 後綴__。以上佈局文件名爲 activity_main.xml
,因此生成的對應類爲 ActivityMainBinding
。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
/*
val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())
*/
binding.user = User("Test", "User")
}
如果您要在 Fragment、ListView 或 RecyclerView 適配器中使用數據綁定項,您可能更願意使用綁定類或 DataBindingUtil 類的 inflate() 方法,如以下代碼示例所示
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
表達式語言
表達式語言與託管代碼中的表達式非常相似。您可以在表達式語言中使用以下運算符和關鍵字:
- 算術運算符
+ - / * %
- 字符串連接運算符
+
- 邏輯運算符
&& ||
- 二元運算符
& | ^
- 一元運算符
+ - ! ~
- 移位運算符
>> >>> <<
- 比較運算符
== > < >= <=
(請注意,<
需要轉義爲<
) instanceof
- 分組運算符
()
- 字面量運算符 - 字符、字符串、數字、
null
- 類型轉換
- 方法調用
- 字段訪問
- 數組訪問
[]
- 三元運算符
?:
示例
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
缺少的運算
您可以在託管代碼中使用的表達式語法中缺少以下運算:
this
super
new
- 顯式泛型調用
Null 合併運算符 (??)
如果左邊運算數不是 null,則 Null 合併運算符 (??) 選擇左邊運算數,如果左邊運算數爲 ,則選擇右邊運算數。
android:text="@{user.displayName ?? user.lastName}"
這在功能上等效於:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
屬性引用
表達式可以使用以下格式在類中引用屬性,這對於__字段、getter 和 ObservableField 對象__都一樣:
android:text="@{user.lastName}"
避免出現 Null 指針異常
__生成的數據綁定代碼會自動檢查有沒有 null 值並避免出現 Null 指針異常。__例如,在表達式 @{user.name} 中,如果 user 爲 Null,則爲 user.name 分配默認值 null。如果您引用 user.age,其中 age 的類型爲 int,則數據綁定使用默認值 0。
集合
爲方便起見,可使用 []
運算符訪問常見集合,例如數組、列表、稀疏列表和映射。
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
注意:要使 XML 不含語法錯誤,您必須轉義 < 字符。例如:不要寫成 List 形式,而是必須寫成__List<String>
__。
還可以使用 object.key
表示法在映射中引用值。例如,以上示例中的 @{map[key]}
可替換爲 @{map.key}
。
字符串
使用單引號括住屬性值,這樣就可以在表達式中使用雙引號,如以下示例所示:
android:text='@{map["firstName"]}'
<!-- 或者 -->
android:text="@{map[`firstName`]}"
資源
可以使用以下語法訪問表達式中的資源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式字符串和複數形式可通過提供參數進行求值:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
當一個複數帶有多個參數時,應傳遞所有參數:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
某些資源需要顯式類型求值,如下表所示:
類型 | — | 表達式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
事件處理
通過數據綁定,您可以編寫從視圖分派的表達式處理事件(例如,
onClick()
方法)。事件特性名稱由監聽器方法的名稱確定,但有一些例外情況
類 | 監聽器 setter | 特性 |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
ZoomControls | setOnClickListener(View.OnClickListener) | android:onClick |
方法引用
事件可以直接綁定到處理腳本方法,類似於爲 Activity 中的方法指定android:onClick 的方式。
與 View onClick 特性相比,一個主要優點是表達式在編譯時進行處理,因此,如果該方法不存在或其簽名不正確,則會收到編譯時錯誤。
方法引用和監聽器綁定之間的主要區別在於:
實際監聽器實現是在綁定數據時創建的,而不是在事件觸發時創建的。
如果您希望在事件發生時對錶達式求值,則應使用監聽器綁定。
示例
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
<?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>
注意:表達式中的方法簽名必須與監聽器對象中的方法簽名完全一致。
監聽器綁定
監聽器綁定是在事件發生時運行的綁定表達式。它們類似於方法引用,但允許您運行任意數據綁定表達式。此功能適用於 Gradle 2.0 版及更高版本的 Android Gradle 插件。
在方法引用中,方法的參數必須與事件監聽器的參數匹配。
在監聽器綁定中,只有您的返回值必須與監聽器的預期返回值相匹配(預期返回值無效除外)。
例子
class Presenter {
fun onSaveClick(task: Task){}
}
<?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>
監聽器綁定提供兩個監聽器參數選項:您可以忽略方法的所有參數,也可以命名所有參數。如果您想命名參數,則可以在表達式中使用這些參數。
例如,上面的表達式可以寫成如下形式:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
如果想在表達式中使用參數,則採用如下形式:
class Presenter {
fun onSaveClick(view: View, task: Task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
可以在 lambda 表達式中使用多個參數:
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
如果您監聽的事件返回類型不是 void 的值,則您的表達式也必須返回相同類型的值。
例如,如果要監聽長按事件,表達式應返回一個布爾值
class Presenter {
fun onLongClick(view: View, task: Task): Boolean { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由於 null
對象而無法對錶達式求值,則數據綁定將返回該類型的默認值。例如,引用類型返回 null
,int
返回 0
,boolean
返回 false
,等等。
如果您需要將表達式與謂詞(例如,三元運算符)結合使用,則可以使用 void
作爲符號。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免使用複雜的監聽器
監聽器表達式功能非常強大,可以使您的代碼非常易於閱讀。另一方面,包含複雜表達式的監聽器會使您的佈局難以閱讀和維護。
這些表達式應該像將可用數據從界面傳遞到回調方法一樣簡單。您應該在從監聽器表達式調用的回調方法中實現任何業務邏輯。
導入、變量和包含
導入
通過導入功能,您可以輕鬆地在佈局文件中引用類。
示例
<data>
<import type="android.view.View"/>
<!-- 類型別名:適用於類名出現衝突的情況-->
<import type="com.example.real.estate.View"
alias="Vista"/>
</data>
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
在表達式中引用靜態字段和方法時,也可以使用導入的類型。以下代碼會導入 MyStringUtils
類,並引用其 capitalize
方法:
<data>
<import type="com.example.MyStringUtils"/>
<variable name="user" type="com.example.User"/>
</data>
…
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
變量
通過變量功能,您可以描述可在綁定表達式中使用的屬性。
您可以在 data
元素中使用多個 variable
元素。每個 variable
元素都描述了一個可以在佈局上設置、並將在佈局文件中的綁定表達式中使用的屬性。
以下示例聲明瞭 user
、image
和 note
變量:
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
變量類型在編譯時進行檢查,因此,如果變量實現 Observable 或者是可觀察集合,則應反映在類型中。如果該變量是不實現 Observable 接口的基類或接口,則變量是“不可觀察的”。
如果不同配置(例如橫向或縱向)有不同的佈局文件,則變量會合並在一起。這些佈局文件之間__不得存在有衝突的變量定義__。
在生成的綁定類中,每個描述的變量都有一個對應的 setter 和 getter。在調用 setter 之前,這些變量一直採用默認的託管代碼值,例如引用類型採用
null
,int
採用0
,boolean
採用false
,等等。系統會根據需要生成名爲
context
的特殊變量,用於綁定表達式。context
的值是根視圖的getContext()
方法中的Context
對象。context
變量會被具有該名稱的顯式變量聲明替換。
包含
通過包含功能,您可以在整個應用中重複使用複雜的佈局。
通過使用應用命名空間和特性中的變量名稱,變量可以從包含的佈局傳遞到被包含佈局的綁定。
以下示例展示了來自 name.xml
和 contact.xml
佈局文件的被包含 user
變量:
<?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">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>
__數據綁定不支持 include 作爲 merge 元素的直接子元素。__例如,以下佈局不受支持:
<?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">
<data>
<variable name="user" type="com.example.User"/>
</data>
<merge><!-- Doesn't work -->
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>
使用可觀察的數據對象
可觀察性是指一個對象將其數據變化通知給其他對象的能力。通過數據綁定庫,您可以讓對象、字段或集合變爲可觀察。
任何普通對象都可用於數據綁定,但修改對象不會自動使界面更新。通過數據綁定,數據對象可在其數據發生更改時通知其他對象,即監聽器。可觀察類有三種不同類型:對象、字段__和__集合。
當其中一個可觀察數據對象綁定到界面並且該數據對象的屬性發生更改時,界面會自動更新。
字段
在創建實現 Observable 接口的類時要完成一些操作,但如果您的類只有少數幾個屬性,則這樣操作的意義不大。在這種情況下,您可以使用通用 Observable 類和以下特定於基元的類,將字段設爲可觀察字段:
- ObservableBoolean
- ObservableByte
- ObservableChar
- ObservableShort
- ObservableInt
- ObservableLong
- ObservableFloat
- ObservableDouble
- ObservableParcelable
可觀測字段是自成一體的可觀測對象。原始版本在訪問操作時避免了裝箱和解裝箱。要使用這種機制,在Java編程語言中創建一個公共的最終屬性,或者在Kotlin中創建一個只讀屬性,如下例所示
class User {
val firstName = ObservableField<String>()
val lastName = ObservableField<String>()
val age = ObservableInt()
}
要訪問字段值,請使用 set() 和 get() 訪問器方法,如下所示:
user.firstName = "Google"
val age = user.age
注意:Android Studio 3.1 及更高版本允許用 LiveData 對象替換可觀察字段,從而爲您的應用提供額外的好處。有關詳情,請參閱使用 LiveData 將數據變化通知給界面。
集合
某些應用使用動態結構來保存數據。可觀察集合允許使用鍵訪問這些結構。當鍵爲引用類型(如 String
)時,ObservableArrayMap
類非常有用,如以下示例所示:
ObservableArrayMap<String, Any>().apply {
put("firstName", "Google")
put("lastName", "Inc.")
put("age", 17)
}
在佈局文件中,也可以使用以字符串爲鍵的Map,如下
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="@{String.valueOf(1 + (Integer)user.age)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
當鍵是整數的時候,ObservableArrayList就很方便:
ObservableArrayList<Any>().apply {
add("Google")
add("Inc.")
add(17)
}
在佈局文件中,允許List使用角標進行索引
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
對象
實現
Observable
接口的類允許註冊監聽器,以便它們接收有關可觀察對象的屬性更改的通知。
Observable 接口具有添加和移除監聽器的機制,但何時發送通知則必須由您決定。
爲便於開發,數據綁定庫提供了用於實現監聽器註冊機制的 BaseObservable 類。實現 BaseObservable 的數據類負責在屬性更改時發出通知。具體操作過程是向 getter 分配 Bindable 註釋,然後在 setter 中調用 notifyPropertyChanged() 方法,如以下示例所示:
class User : BaseObservable() {
@get:Bindable
var firstName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.firstName)
}
@get:Bindable
var lastName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.lastName)
}
}
數據綁定在模塊包中生成一個名爲 BR 的類,該類包含用於數據綁定的資源的 ID。在編譯期間,Bindable 註釋會在 BR 類文件中生成一個條目。如果數據類的基類無法更改,則 Observable 接口可以使用 PropertyChangeRegistry 對象實現,以便有效地註冊和通知監聽器。
生成的綁定類
生成的綁定類將佈局變量與佈局中的視圖關聯起來。綁定類的名稱和包可以自定義。所有生成的綁定類都是從 ViewDataBinding 類繼承而來的。
創建綁定對象
在對佈局進行擴充後,應儘快創建綁定對象,以確保視圖層次結構在通過表達式與佈局內的視圖綁定之前不會被修改。
將對象綁定到佈局的最常用方法是在綁定類上使用靜態方法。使用綁定類的 inflate()
方法來擴充視圖層次結構並將對象綁定到該層次結構,如以下示例所示
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
}
inflate() 方法還有另外一個版本,這個版本不僅使用 LayoutInflater 對象,還使用 ViewGroup 對象,如以下示例所示:
val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false)
如果佈局是使用其他機制擴充的,可單獨綁定,如下所示:
val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)
如果您要在 Fragment、ListView 或 RecyclerView 適配器中使用數據綁定項,您可能更願意使用綁定類的 inflate() 方法或 DataBindingUtil 類,如以下代碼示例所示:
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
帶ID的視圖
數據綁定庫會針對佈局中具有 ID 的每個視圖在綁定類中創建不可變字段。例如,數據綁定庫會根據以下佈局創建 TextView
類型的 firstName
和 lastName
字段:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<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:id="@+id/firstName"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:id="@+id/lastName"/>
</LinearLayout>
</layout>
該庫一次性從視圖層次結構中提取包含 ID 的視圖。相較於針對佈局中的每個視圖調用 findViewById()
方法,這種機制速度更快。
變量
數據綁定庫爲佈局中聲明的每個變量生成訪問器方法。例如,以下佈局在綁定類中針對 user
、image
和 note
變量生成了 setter 和 getter 方法:
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
ViewStubs
與普通視圖不同,
ViewStub
對象初始是一個不可見視圖。當它們顯示出來或者獲得明確指示進行擴充時,它們會通過擴充另一個佈局在佈局中完成自我取代。由於
ViewStub
實際上會從視圖層次結構中消失,因此綁定對象中的視圖也必須消失,才能通過垃圾回收進行回收。由於視圖是最終結果,因此ViewStubProxy
對象將取代生成的綁定類中的ViewStub
,讓您能夠訪問ViewStub
(如果存在),同時還能訪問ViewStub
進行擴充後的擴充版視圖層次結構。在擴充其他佈局時,必須爲新佈局建立綁定。因此,
ViewStubProxy
必須監聽ViewStub
OnInflateListener
並在必要時建立綁定。由於在給定時間只能有一個監聽器,因此ViewStubProxy
允許您設置OnInflateListener
,它將在建立綁定後調用這個監聽器。
即時綁定
當可變或可觀察對象發生更改時,綁定會按照計劃在下一幀之前發生更改。但有時必須立即執行綁定。要強制執行,請使用
executePendingBindings()
方法。
高級綁定
動態變量
有時,系統並不知道特定的綁定類。例如,針對任意佈局運行的 RecyclerView.Adapter
不知道特定綁定類。在調用 onBindViewHolder()
方法時,仍必須指定綁定值。
在以下示例中,RecyclerView
綁定到的所有佈局都有 item
變量。BindingHolder
對象具有一個 getBinding()
方法,這個方法返回 ViewDataBinding
基類。
override fun onBindViewHolder(holder: BindingHolder, position: Int){
item: T = items.get(position)
holder.binding.setVariable(BR.item, item);
holder.binding.executePendingBindings();
}
注意:數據綁定庫在模塊包中生成一個名爲
BR
的類,其中包含用於數據綁定的資源的 ID。在上例中,該庫自動生成BR.item
變量。
後臺線程
可以在後臺線程中更改數據模型,但前提是這個模型不是集合。數據綁定會在求值過程中對每個變量/字段進行本地化,以避免出現併發問題。
自定義綁定類名稱
默認情況下,綁定類是根據佈局文件的名稱生成的,以大寫字母開頭,移除下劃線 ( _ ),將後一個字母大寫,最後添加後綴 Binding。該類位於模塊包下的 databinding
包中。例如,佈局文件 contact_item.xml
會生成 ContactItemBinding
類。如果模塊包是 com.example.my.app
,則綁定類放在 com.example.my.app.databinding
包中。
通過調整 data
元素的 class
特性,綁定類可重命名或放置在不同的包中。例如,以下佈局在當前模塊的 databinding
包中生成 ContactItem
綁定類:
<data class="ContactItem">
…
</data>
您可以在類名前添加句點和前綴,從而在其他文件包中生成綁定類。以下示例在模塊包中生成綁定類:
<data class=".ContactItem">
…
</data>
您還可以使用完整軟件包名稱來生成綁定類。以下示例在 com.example
包中創建 ContactItem
綁定類:
<data class="com.example.ContactItem">
…
</data>
綁定適配器
綁定適配器負責發出相應的框架調用來設置值。例如,設置屬性值就像調用
setText()
方法一樣。再比如,設置事件監聽器就像調用setOnClickListener()
方法。數據綁定庫允許您通過使用適配器指定爲設置值而調用的方法、提供您自己的綁定邏輯,以及指定返回對象的類型。
設置屬性值
只要綁定值發生更改,生成的綁定類就必須使用綁定表達式在視圖上調用 setter 方法。您可以允許數據綁定庫自動確定方法、顯式聲明方法或提供選擇方法的自定義邏輯。
自動選擇方法
指定自定義方法名稱
某些特性擁有名稱不符的 setter。在這些情況下,某個特性可能會使用 BindingMethods
註釋與 setter 相關聯。
註釋與類一起使用,可以包含多個 BindingMethod
註釋,每個註釋對應一個重命名的方法。綁定方法是可添加到應用中任何類的註釋。
在以下示例中,android:tint
特性與 setImageTintList(ColorStateList)
方法相關聯,而不與 setTint()
方法相關聯:
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])
提供自定義邏輯
__某些特性需要自定義綁定邏輯。__例如,android:paddingLeft
特性沒有關聯的 setter,而是提供了 setPadding(left, top, right, bottom)
方法。使用 BindingAdapter
註釋的靜態綁定適配器方法支持自定義特性 setter 的調用方式。
Android 框架類的特性已經創建了 BindingAdapter
註釋。例如,以下示例展示了 paddingLeft
特性的綁定適配器:
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
第一個參數用於確定與特性關聯的視圖類型;第二個參數用於確定在給定特性的綁定表達式中接受的類型。
綁定適配器對其他類型的自定義很有用。例如,可以通過工作器線程調用自定義加載程序來加載圖片。
出現衝突時,您定義的綁定適配器會替換由 Android 框架提供的默認適配器。
您還可以使用接收多個特性的適配器,如以下示例所示:
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
可以在佈局中使用適配器,如以下示例所示。請注意,@drawable/venueError
引用應用中的資源。使用 @{}
將資源括起來可使其成爲有效的綁定表達式。
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}" />
注意:數據綁定庫在匹配時會忽略自定義命名空間。
如果 ImageView
對象同時使用了 imageUrl
和 error
,並且 imageUrl
是字符串,error
是 Drawable
,則會調用適配器。
如果您希望在設置了任意特性時調用適配器,則可以將適配器的可選 requireAll
標記設置爲 false
,如以下示例所示:
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
注意:出現衝突時,綁定適配器會替換默認的數據綁定適配器。
綁定適配器方法可以選擇性在處理程序中使用舊值。同時採用舊值和新值的方法應該先爲特性聲明所有舊值,然後再聲明新值,如以下示例所示:
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
if (oldPadding != newPadding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
}
事件處理程序只能與具有一種抽象方法的接口或抽象類一起使用,如以下示例所示:
@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
view: View,
oldValue: View.OnLayoutChangeListener?,
newValue: View.OnLayoutChangeListener?
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue)
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue)
}
}
}
按如下所示在佈局中使用此事件處理腳本:
<View
android:onLayoutChange="@{() -> handler.layoutChanged()}"/>
__當監聽器有多個方法時,必須將它拆分爲多個監聽器。__例如,View.OnAttachStateChangeListener
有兩個方法:onViewAttachedToWindow(View)
和 onViewDetachedFromWindow(View)
。該庫提供了兩個接口,用於區分它們的特性和處理腳本:
// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
fun onViewDetachedFromWindow(v: View)
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
fun onViewAttachedToWindow(v: View)
}
因爲更改一個監聽器也會影響另一個監聽器,所以需要適用於其中一個特性或同時適用於這兩個特性的適配器。您可以在註釋中將
requireAll
設置爲false
,以指定並非必須爲每個特性都分配綁定表達式,如以下示例所示:
@BindingAdapter(
"android:onViewDetachedFromWindow",
"android:onViewAttachedToWindow",
requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
val newListener: View.OnAttachStateChangeListener?
newListener = if (detach == null && attach == null) {
null
} else {
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
attach.onViewAttachedToWindow(v)
}
override fun onViewDetachedFromWindow(v: View) {
detach.onViewDetachedFromWindow(v)
}
}
}
val oldListener: View.OnAttachStateChangeListener? =
ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener)
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener)
}
}
}
以上示例比一般情況稍微複雜一些,因爲
View
類使用addOnAttachStateChangeListener()
和removeOnAttachStateChangeListener()
方法,而非OnAttachStateChangeListener
的 setter 方法。android.databinding.adapters.ListenerUtil
類有助於跟蹤以前的監聽器,以便在綁定適配器中將它們移除。通過用
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
註釋接口OnViewDetachedFromWindow
和OnViewAttachedToWindow
,數據綁定代碼生成器知道只應在運行 Android 3.1(API 級別 12)及更高級別(addOnAttachStateChangeListener()
方法支持的相同版本)時生成監聽器。
對象轉換
自動對象轉換
當綁定表達式返回 Object
時,庫會選擇用於設置屬性值的方法。Object
會轉換爲所選方法的參數類型。對於使用 ObservableMap
類存儲數據的應用,這種行爲非常便捷,如以下示例所示:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
注意:您還可以使用 object.key
表示法引用映射中的值。例如,以上示例中的 @{userMap["lastName"]}
可替換爲 @{userMap.lastName}
。
表達式中的
userMap
對象會返回一個值,該值會自動轉換爲用於設置android:text
屬性值的setText(CharSequence)
方法中的參數類型。如果參數類型不明確,則必須在表達式中強制轉換返回類型。
自定義轉換
在某些情況下,需要在特定類型之間進行自定義轉換。
例如,視圖的 android:background
特性需要 Drawable
,但指定的 color
值是整數。以下示例展示了某個特性需要 Drawable
,但結果提供了一個整數:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
每當需要 Drawable
且返回整數時,int
都應轉換爲 ColorDrawable
。您可以使用帶有 BindingConversion
註釋的靜態方法完成這個轉換,如下所示
@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)
但是,__綁定表達式中提供的值類型必須保持一致。__您不能在同一個表達式中使用不同的類型,如以下示例所示:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
將佈局視圖綁定到架構組件
使用LiveData將數據變化通知給頁面
與實現
Observable
的對象(例如可觀察字段)不同,LiveData
對象瞭解訂閱數據更改的觀察器的生命週期。
要將 LiveData
對象與綁定類一起使用,需要指定生命週期所有者來定義 對象的範圍。
以下示例在綁定類實例化後將 Activity 指定爲生命週期所有者:
class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Inflate view and obtain an instance of the binding class.
val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)
}
}
您可以根據使用 ViewModel 管理界面相關數據中所述,使用 ViewModel 組件來將數據綁定到佈局。在 ViewModel 組件中,您可以使用 LiveData 對象轉換數據或合併多個數據源。
以下示例展示瞭如何在 ViewModel 中轉換數據:
class ScheduleViewModel : ViewModel() {
val userName: LiveData
init {
val result = Repository.userName
userName = Transformations.map(result) { result -> result.value }
}
}
使用ViewModel管理界面相關數據
要將 ViewModel
組件與數據綁定庫一起使用,必須實例化從 類繼承而來的組件,獲取綁定類的實例,並將您的 組件分配給綁定類中的屬性。
以下示例展示瞭如何將組件與庫結合使用:
class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Obtain the ViewModel component.
UserModel userModel = ViewModelProviders.of(getActivity())
.get(UserModel.class)
// Inflate view and obtain an instance of the binding class.
val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)
// Assign the component to a property in the binding class.
binding.viewmodel = userModel
}
}
在佈局中,使用綁定表達式將 ViewModel
組件的屬性和方法分配給對應的視圖,如以下示例所示:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />
使用Observable ViewModel以更好地控制綁定適配器
使用實現
Observable
的ViewModel
組件可讓您更好地控制應用中的綁定適配器。例如,這種模式可讓您更多地控制數據更改時的通知,您還可以指定自定義方法來設置雙向數據綁定中的屬性值。
要實現可觀察的 ViewModel
組件,您必須創建一個從 類繼承而來並實現 Observable
接口的類。您可以使用 addOnPropertyChangedCallback()
和 removeOnPropertyChangedCallback()
方法提供觀察器訂閱或取消訂閱通知時的自定義邏輯。您還可以在 notifyPropertyChanged()
方法中提供屬性更改時運行的自定義邏輯。以下代碼示例展示瞭如何實現一個可觀察的 ViewModel
:
/**
* A ViewModel that is also an Observable,
* to be used with the Data Binding Library.
*/
open class ObservableViewModel : ViewModel(), Observable {
private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
override fun addOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
override fun removeOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* Notifies observers that all properties of this instance have changed.
*/
fun notifyChange() {
callbacks.notifyCallbacks(this, 0, null)
}
/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
雙向數據綁定
使用單向數據綁定,您可以爲特性設置值,並設置對該特性更改作出反應的監聽器
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>
雙向數據綁定爲此過程提供了一個快捷方式:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>
@={}
表示法接收屬性的數據更改並同時監聽用戶更新,其中重要的是包含“=”符號。
爲了應對後備數據的更改,您可以將您的佈局變量設置爲 Observable
(通常爲 BaseObservable
)的實現,並使用 @Bindable
註釋,如以下代碼段所示:
class LoginViewModel : BaseObservable {
// val data = ...
@Bindable
fun getRememberMe(): Boolean {
return data.rememberMe
}
fun setRememberMe(value: Boolean) {
// Avoids infinite loops.
if (data.rememberMe != value) {
data.rememberMe = value
// React to the change.
saveData()
// Notify observers of a new value.
notifyPropertyChanged(BR.remember_me)
}
}
}
使用自定義特性的雙向數據綁定
如果您希望結合使用雙向數據綁定和自定義特性,則需要使用 @InverseBindingAdapter
和 @InverseBindingMethod
註釋。
例如,如果要在名爲 MyView
的自定義視圖中對 "time"
特性啓用雙向數據綁定,請完成以下步驟:
-
使用
@BindingAdapter
,對用來設置初始值並在值更改時進行更新的方法進行註釋:@BindingAdapter("time") @JvmStatic fun setTime(view: MyView, newValue: Time) { // Important to break potential infinite loops. if (view.time != newValue) { view.time = newValue } }
-
使用
@InverseBindingAdapter
對從視圖中讀取值的方法進行註釋:@InverseBindingAdapter("time") @JvmStatic fun getTime(view: MyView) : Time { return view.getTime() }
此時,數據綁定知道在數據發生更改時要執行的操作(調用使用
@BindingAdapter
註釋的方法)以及當 view 視特性發生更改時要調用的內容(調用InverseBindingListener
)。但是,它不知道特性何時或如何更改。爲此,您需要在視圖上設置監聽器。這可以是與您的自定義視圖相關聯的自定義監聽器,也可以是通用事件,例如失去焦點或文本更改。將
@BindingAdapter
註釋添加到設置監聽器(用來監聽屬性更改)的方法中:@BindingAdapter("app:timeAttrChanged") @JvmStatic fun setListeners( view: MyView, attrChange: InverseBindingListener ) { // Set a listener for click, focus, touch, etc. }
該監聽器包含一個
InverseBindingListener
參數。您可以使用InverseBindingListener
告知數據綁定系統,特性已更改。然後,該系統可以開始調用使用@InverseBindingAdapter
註釋的方法,依此類推。注意:每個雙向綁定都會生成“合成事件特性”。該特性與基本特性具有相同的名稱,但包含後綴
"AttrChanged"
。合成事件特性允許庫創建使用@BindingAdapter
註釋的方法,以將事件監聽器與相應的View
實例相關聯。
轉換器
如果綁定到 View
對象的變量需要設置格式、轉換或更改後才能顯示,則可以使用 Converter
對象。
以顯示日期的 EditText
對象爲例:
<EditText
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>
viewmodel.birthDate
屬性包含Long
類型的值,因此需要使用轉換器設置格式。
由於使用了雙向表達式,因此還需要使用反向轉換器,以告知庫如何將用戶提供的字符串轉換回後備數據類型(在本例中爲 Long
)。此過程是通過向其中一個轉換器添加 @InverseMethod
註釋並讓此註釋引用反向轉換器來完成的。以下代碼段顯示了此配置的一個示例:
object Converter {
@InverseMethod("stringToDate")
fun dateToString(
view: EditText, oldValue: Long,
value: Long
): String {
// Converts long to String.
}
fun stringToDate(
view: EditText, oldValue: String,
value: String
): Long {
// Converts String to long.
}
}
使用雙向數據綁定的無限循環
使用雙向數據綁定時,請注意不要引入無限循環。當用戶更改特性時,系統會調用使用
@InverseBindingAdapter
註釋的方法,並且該值將分配給後備屬性。繼而調用使用@BindingAdapter
註釋的方法,從而觸發對使用@InverseBindingAdapter
註釋的方法的另一個調用,依此類推。
因此,通過比較使用 @BindingAdapter
註釋的方法中的新值和舊值,可以打破可能出現的無限循環
雙向特性
當您使用下表中的特性時,該平臺提供對雙向數據綁定的內置支持。有關平臺如何提供此類支持的詳細信息,請參閱相應綁定適配器的實現:
類 | 特性 | 綁定適配器 |
---|---|---|
AdapterView |
android:selectedItemPosition android:selection |
AdapterViewBindingAdapter |
CalendarView |
android:date |
CalendarViewBindingAdapter |
CompoundButton |
android:checked |
CompoundButtonBindingAdapter |
DatePicker |
android:year android:month android:day |
DatePickerBindingAdapter |
NumberPicker |
android:value |
NumberPickerBindingAdapter |
RadioButton |
android:checkedButton |
RadioGroupBindingAdapter |
RatingBar |
android:rating |
RatingBarBindingAdapter |
SeekBar |
android:progress |
SeekBarBindingAdapter |
TabHost |
android:currentTab |
TabHostBindingAdapter |
TextView |
android:text |
TextViewBindingAdapter |
TimePicker |
android:hour android:minute |
TimePickerBindingAdapter |