Data Binding 系列(三)佈局和繫結表示式
這種表示式語言(expression language
)使我們可以使用表示式處理 view 的事件。Data Binding
庫會自動生成繫結類(binding class
)用來處理 view 和 data 的繫結關係。
使用Data Binding
的佈局檔案和傳統的佈局檔案稍有不同,它的根標籤是layout
,裡面會有一個data
子標籤和一個根 view 子標籤。這個根 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
標籤中宣告的user
變數,將會在繫結表示式中用到。
<variable name="user" type="com.example.User" /> 複製程式碼
繫結表示式用於為屬性賦值,它使用的語法是@{}
。在下面的例子中,TextView
控制元件的屬性text
,被賦值為user
變數的firstName
屬性值:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}" /> 複製程式碼
資料來源
假設現在有一個描述User
實體的資料物件:
public class User { public final String firstName; public final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } 複製程式碼
這個資料類的成員屬性都是不可變的、是 public 的。它還有另外一種寫法,成員屬性是 private 的,並且提供訪問它們的方法:
public class User { private final String firstName; private final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } } 複製程式碼
從資料繫結的角度來看,上面兩個實體類是等價的。用於給android:text
屬性賦值的表示式@{user.firstName}
,會自動讀取前一個類的firstName
屬性,或者呼叫後一個類的getFirstName()
方法。而且,如果firstName()
方法存在,也會呼叫這個方法。
可繫結的資料(Binding data
)
每個佈局檔案都會生成一個對應的繫結類。預設的繫結類的名字是檔名轉為駝峰寫法並加上字尾Binding
。比如檔名是activity_main.xml
,對應的繫結類名為ActivityMainBinding
。這個繫結類儲存了資料變數和 view 屬性的繫結關係,並且知道如何為 view屬性賦值。推薦的方式是在載入佈局的時候建立繫結類,如下所示:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityMainBinding = DataBindingUtil.setContentView( this, R.layout.activity_main) binding.user = User("Test", "User") } 複製程式碼
另外,還可以使用inflate()
方法建立繫結類:
val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater()) 複製程式碼
如果是在Fragment
、ListView
、RecyclerView
中使用Data Binding
,你可能更傾向於使用inflate()
方法或DataBindingUtil
類,如下所示:
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false) // or val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false) 複製程式碼
表示式語言(expression language
)
1 常用特性
表示式語言看起來很像程式碼裡面的表示式。你可以在表示式中使用如下的操作符和關鍵字:
-
計算相關運算子
+ - / * %
-
字元連線符
+
-
邏輯運算子
&& ||
-
二元運算子
& | ^
-
一元運算子
+ - ! ~
-
移位運算子
>> >>> <<
-
比較運算子
== > < >= <=
(注:<
運算子需要寫成<
) -
instance of
-
()
- 字元、字串、數字、null
- 強轉
- 方法呼叫
- 屬性訪問
-
陣列訪問
[]
-
三元元素符
?:
舉例:
android:text="@{String.valueOf(index + 1)}" android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}" android:transitionName='@{"image_" + id}' 複製程式碼
2 不可用操作
下面操作只能在程式碼裡使用,不能用於表示式語法:
this super new
3 空結合運算子(??
)
空結合運算子左邊表示式如果不為空,使用左邊表示式結果,否則使用右邊表示式結果
android:text="@{user.displayName ?? user.lastName}" 複製程式碼
它等價於:
android:text="@{user.displayName != null ? user.displayName : user.lastName}" 複製程式碼
4 屬性引用
使用繫結表示式,可以引用一個類的成員變數、get 方法、ObservableField
:
android:text="@{user.lastName}" 複製程式碼
5 避免空指標異常
生成的繫結類會自動判空從而避免空指標異常。比如,表示式@{user.name}
,如果user
是空,user.name
會被賦予預設值null
。如果引用的是user.age
,age
的型別是int
,那麼會使用0
作為預設值。
6 集合
常用的集合,如陣列、list、sparse list、map 等,可以方便地使用[]
操作符訪問它們的元素。
<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<String>
應該寫成List<String>
。
訪問 map 中的元素,除了可以使用@{map[key]}
,也可以使用@{map.key}
。
7 字串的寫法
可以使用單引號包裹屬性,在表示式中使用雙引號,如下所示:
android:text='@{map["firstName"]}' 複製程式碼
也可以使用雙引號包裹屬性,在表示式中使用單引號,如下所示:
android:text="@{map[`firstName`]}" 複製程式碼
8 訪問資源
可以在表示式中使用如下語法訪問資源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" 複製程式碼
9 事件處理
Data Binding
可以讓我們在表示式中處理 view 分發的事件,比如onClick()
。屬性的名字取決於監聽器方法的名字,比如,View.OnClickListener
有一個onClick()
方法,所以對應的屬性名就是android:onClick
。
處理 view 事件有兩種方法:
null listener binding
- 方法引用
事件可以和方法直接繫結,這種方式與android:onClick
可以和 activity 中一個方法繫結很類似。這種方式的優勢是,繫結表示式是在編譯期間處理的,如果方法不存在或者簽名不匹配,會直接報錯。
和監聽器繫結不同的是,方法引用的方式會在資料繫結的時候建立監聽器,監聽器繫結則是在收到事件的時候建立監聽器。
示例如下:
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> 複製程式碼
注: 表示式中的方法簽名必須與監聽器中的方法簽名一致。
-
監聽器繫結(
listener binding
)
監聽器繫結的方式,只有在收到事件的時候才會執行。它和方法引用很像,但是它可以使用任意的表示式。這個特性在 Gradle 2.0 及以後可以使用。
方法引用的方式,要求方法的簽名必須和監聽器的方法簽名一致。但是監聽器繫結的方式,只要求返回值一致即可。例如,假如下面的 Presenter 類有一個onSaveClick()
方法:
class Presenter { fun onSaveClick(task: Task){} } 複製程式碼
onSaveClick()
可以與android:onClick
繫結,如下所示:
<?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> 複製程式碼
在上面這個例子中,我們沒有定義 view 引數。監聽器繫結提供了兩種選擇:要麼忽略所有引數,要麼顯式寫出所有引數。如果你喜歡寫出引數,如下所示:
android:onClick="@{(view) -> presenter.onSaveClick(task)}" 複製程式碼
如果你需要使用這些引數,如下所示:
class Presenter { fun onSaveClick(view: View, task: Task){} } 複製程式碼
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}" 複製程式碼
同時,也可以有多個引數,如下所示:
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,int 型別返回 0,布林型別返回 false。
Imports、variables、includes
Data Binding
提供了諸如imports
、variables
、includes
等特性。imports
用於匯入所需要的類,方便引用;variable
用於定義一個變數,方便在繫結表示式中使用。includes
使我們可以複用佈局。
- Imposts
下面這個例子展示了匯入View
這個類到佈局檔案中:
<data> <import type="android.view.View"/> </data> 複製程式碼
匯入的目的就是為了方便在繫結表示式中使用。下面的例子展示了在表示式中引用View
類的兩個常量VISIBLE
和GOEN
:
<TextView android:text="@{user.lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/> 複製程式碼
-
- 類型別名
如果匯入的兩個類名字相同,可以為其中一個或兩個起別名以區分:
<import type="android.view.View"/> <import type="com.example.real.estate.View" alias="Vista"/> 複製程式碼
注:java.lang.*
下面的類會自動匯入。
- Variables
變數的宣告用於在繫結表示式中使用,如下所示,聲明瞭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> 複製程式碼
自動生成的繫結類,包含了這些變數的 get 和 set 方法。這些變數都會有預設值,引用型別預設值是null
,int 型別預設值是 0,布林型別預設值是 false。
- Includes
變數可以傳遞給include
佈局,如下所示,user
變數傳遞給了name.xml
和contact.xml
兩個佈局:
<?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> 複製程式碼
Data binding
不支援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> 複製程式碼