寫在前面
要學習新東西,最好的辦法是先學會如何使用。所以,本文僅作 Android Data Binding 的介紹并結合 DataBindingDemo 來理解它的用法,后續再對其原理進行深入探討。
簡介
Data binding 在2015年7月發布的Android Studio v1.3.0 版本上引入,在2016年4月Android Studio v2.0.0 上正式支持。目前為止,Data Binding 已經支持雙向綁定了。
Databinding 是一個實現數據和UI綁定的框架,是一個實現 MVVM 模式的工具,有了 Data Binding,在Android中也可以很方便的實現MVVM開發模式。
Data Binding 是一個support庫,最低支持到Android 2.1(API Level 7+)。
Data Binding 之前,我們不可避免地要編寫大量的毫無營養的代碼,如 findViewById()、setText(),setVisibility(),setEnabled() 或 setOnClickListener() 等,通過 Data Binding , 我們可以通過聲明式布局以精簡的代碼來綁定應用程序邏輯和布局,這樣就不用編寫大量的毫無營養的代碼了。
構建環境
-
首先,確保能使用Data Binding,需要下載最新的 Support repository。否則可能報錯,如圖:
-
在模塊的build.gradle文件中添加dataBinding配置
android { .... dataBinding { enabled = true } }
注意:如果app依賴了一個使用 Data Binding 的庫,那么app module 的 build.gradle 也必須配置 Data Binding。
Data Binding 布局文件 - (View)
Data binding 的布局文件與傳統布局文件有一點不同。它以一個 layout 標簽作為根節點,里面是 data 標簽與 view 標簽。view 標簽的內容就是不使用 Data Binding 時的普通布局文件內容。以下是一個例子:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<!-- 變量user, 描述了一個布局中會用到的屬性 -->
<variable name="user" type="com.connorlin.databinding.model.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>
數據對象 - (Model)
假設你有一個 plain-old Java object(POJO) 的 User 對象。
public class User {
private final String mFirstName;
private final String mLastName;
private int mAge;
public User(String firstName, String lastName, int age) {
mFirstName = firstName;
mLastName = lastName;
mAge = age;
}
}
或者是 JavaBean 對象:
public class User {
private final String mFirstName;
private final String mLastName;
private int mAge;
public User(String firstName, String lastName, int age) {
mFirstName = firstName;
mLastName = lastName;
mAge = age;
}
public String getFirstName() {
return mFirstName;
}
public String getLastName() {
return mLastName;
}
public int getAge() {
return mAge;
}
}
從 Data Binding 的角度看,這兩個類是一樣的。用于 TextView 的 android:text
屬性的表達式@{user.firstName}
,會讀取 POJO 對象的 firstName
字段以及 JavaBeans 對象的 getFirstName()
方法。
綁定數據 - (ViewModel)
在默認情況下,會基于布局文件生成一個繼承于 ViewDataBinding
的 Binding 類,將它轉換成帕斯卡命名并在名字后面接上Binding
。例如,布局文件叫 main_activity.xml
,所以會生成一個 MainActivityBinding
類。這個類包含了布局文件中所有的綁定關系,會根據綁定表達式給布局文件賦值。在 inflate 的時候創建 binding 的方法如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ActivityBaseBinding 類是自動生成的
ActivityBaseBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_base);
User user = new User("Connor", "Lin");
// 所有的 set 方法也是根據布局中 variable 名稱生成的
binding.setUser(user);
}
事件處理
本部分源碼請參考 DataBindingDemo -> EventActivity
部分。
類似于 android:onClick 可以指定 Activity 中的函數,Data Binding 也允許處理從視圖中發送的事件。
有兩種實現方式:
- 方法調用
- 監聽綁定
二者主要區別在于方法調用在編譯時處理,而監聽綁定于事件發生時處理。
方法調用
相較于 android:onClick ,它的優勢在于表達式會在編譯時處理,如果函數不存在或者函數簽名不對,編譯將會報錯。
以下是個例子:
public class EventHandler {
private Context mContext;
public EventHandler(Context context) {
mContext = context;
}
public void onClickFriend(View view) {
Toast.makeText(mContext, "onClickFriend", Toast.LENGTH_LONG).show();
}
}
表達式如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handler"
type="com.connorlin.databinding.handler.EventHandler"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handler::onClickFriend}"/>
<!-- 注意:函數名和監聽器對象必須對應 -->
<!-- 函數調用也可以使用 `.` , 如handler.onClickFriend , 不過已棄用 -->
</LinearLayout>
</layout>
監聽綁定
監聽綁定在事件發生時調用,可以使用任意表達式
此功能在 Android Gradle Plugin version 2.0 或更新版本上可用.
在方法引用中,方法的參數必須與監聽器對象的參數相匹配。在監聽綁定中,只要返回值與監聽器對象的預期返回值相匹配即可。
以下是個例子:
public void onTaskClick(Task task) {
task.run();
}
表達式如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handler" type="com.connorlin.databinding.handler.EventHandler"/>
<variable
name="task" type="com.connorlin.databinding.task.Task"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> handler.onTaskClick(task)}"/>
</LinearLayout>
</layout>
當一個回調函數在表達式中使用時,數據綁定會自動為事件創建必要的監聽器并注冊監聽。
關于參數
- 參數有兩種選擇:要么不寫,要么就要寫全。
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> handler.onTaskClick(task)}" />
或
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{(view) -> handler.onTaskClick(task)}"/>
- lambda 表達式可添加一個或多個參數,同時參數可任意命名
public class EventHandler {
public void onTaskClickWithParams(View view, Task task) {
task.run();
}
}
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{(theview) -> handler.onTaskClickWithParams(theview, task)}" />
或者
public class EventHandler {
public void onCompletedChanged(Task task, boolean completed) {
if(completed) {
task.run();
}
}
}
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> handler.onCompletedChanged(task, isChecked)}" />
表達式結果有默認值 null、0、false等等
表達式中可以使用void
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}" />
關于表達式
復雜的表達式會使布局難以閱讀和維護,這種情況我們最好將業務邏輯寫到回調函數中
也有一些特殊的點擊事件 我們需要使用不同于 android:onClick 的屬性來避免沖突。
下面是一些用來避免沖突的屬性:
Class | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
布局詳情
本部分源碼請參考 DataBindingDemo -> CombineActivity
部分
導入(Imports)
- data 標簽內可以有多個 import 標簽。你可以在布局文件中像使用 Java 一樣導入引用
<data>
<import type="android.view.View"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
- 當類名發生沖突時,可以使用 alias
<import type="android.view.View"/>
<import type="com.connorlin.databinding.ui.View" alias="AliasView"/>
- 導入的類型也可以用于變量的類型引用和表達式中
<data>
<import type="com.connorlin.databinding.model.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
注意:Android Studio 還沒有對導入提供自動補全的支持。你的應用還是可以被正常編譯,要解決這個問題,你可以在變量定義中使用完整的包名。
- 導入也可以用于在表達式中使用靜態方法
public class MyStringUtils {
public static String capitalize(final String word) {
if (word.length() > 1) {
return String.valueOf(word.charAt(0)).toUpperCase() + word.substring(1);
}
return word;
}
}
<data>
<import type="com.connorlin.databinding.utils.MyStringUtils"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
</data>
…
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
- java.lang.* 包中的類會被自動導入,可以直接使用,例如, 要定義一個 String 類型的變量
<variable name="test" type="String" />
變量 Variables
- data 標簽中可以有任意數量的 variable 標簽。每個 variable 標簽描述了會在 binding 表達式中使用的屬性。
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
- 可以在表達式中直接引用帶 id 的 view,引用時采用駝峰命名法。
<TextView
android:id="@+id/first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={user.firstName}" />
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{firstName.getVisibility() == View.GONE ? View.GONE : View.VISIBLE}" />
<!-- 這里TextView直接引用第一次TextView,firstName為id 的駝峰命名 -->
- binding 類會生成一個命名為 context 的特殊變量(其實就是 rootView 的 getContext() ) 的返回值),這個變量可用于表達式中。 如果有名為 context 的變量存在,那么生成的這個 context 特殊變量將被覆蓋。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{handler.loadString(context)}"/>
public String loadString(Context context) {
// 使用生成的context變量
return context.getResources().getString(R.string.string_from_context);
}
自定義綁定類名
默認情況下,binding 類的名稱取決于布局文件的命名,以大寫字母開頭,移除下劃線,后續字母大寫并追加 “Binding” 結尾。這個類會被放置在 databinding 包中。舉個例子,布局文件 contact_item.xml 會生成 ContactItemBinding 類。如果 module 包名為 com.example.my.app ,binding 類會被放在 com.example.my.app.databinding 中。
通過修改 data 標簽中的 class 屬性,可以修改 Binding 類的命名與位置。舉個例子:
<data class="CustomBinding">
...
</data>
以上會在 databinding 包中生成名為 CustomBinding 的 binding 類。如果需要放置在不同的包下,可以在前面加 “.”
:
<data class=".CustomBinding">
...
</data>
這樣的話, CustomBinding 會直接生成在 module 包下。如果提供完整的包名,binding 類可以放置在任何包名中:
<data class="com.example.CustomBinding">
...
</data>
Includes
在使用應用命名空間的布局中,變量可以傳遞到任何 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">
<data>
<variable name="user" type="com.connorlin.databinding.model.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/include"
app:user="@{user}"/>
</LinearLayout>
</layout>
需要注意, activity_combine.xml 與 include.xml 中都需要聲明 user 變量。
Data binding 不支持直接包含 merge
節點。舉個例子, 以下的代碼<font color = "red">不能正常運行 </font>:
<?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.connorlin.databinding.model.User"/>
</data>
<merge>
<include layout="@layout/include"
app:user="@{user}"/>
</merge>
</layout>
表達式語言
通用特性
表達式語言與 Java 表達式有很多相似之處。下面是相同之處:
- 數學計算 + - / * %
- 字符串連接 +
- 邏輯 && ||
- 二進制 & | ^
- 一元 + - ! ~
- 位移 >> >>> <<
- 比較 == > < >= <=
- instanceof
- 組 ()
- 字面量 - 字符,字符串,數字, null
- 類型轉換
- 函數調用
- 字段存取
- 數組存取 []
- 三元運算符 ?:
例子:
<!-- 內部使用字符串 & 字符拼接-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`Age :` + String.valueOf(user.age)}"/>
<!-- 三目運算-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
在xml中轉義是不可避免的,如 : 使用“&&”是編譯不通過的,需要使用轉義字符 "&&"
附:常用的轉義字符
顯示結果 | 描述 | 轉義字符 | 十進制 |
---|---|---|---|
空格 | ? | ? | |
< | 小于號 | < | < |
> | 大于號 | > | > |
& | 與號 | & | & |
" | 引號 | " | " |
' | 撇號 | ' | ' |
× | 乘號 | × | × |
÷ | 除號 | ÷ | ÷ |
不支持的操作符
一些 Java 中的操作符在表達式語法中不能使用。
- this
- super
- new
- 顯式泛型調用
<T>
Null合并運算符
Null合并運算符 ??
會在非 null 的時候選擇左邊的操作,反之選擇右邊。
android:text="@{user.lastName ?? `Default LastName`}"
等同于
android:text="@{user.lastName != null ? user.lastName : `Default LastName`}"
容器類
通用的容器類:數組,lists,sparse lists,和 maps,可以用 []
操作符來存取
<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]}"
字符串常量
使用單引號把屬性包起來,就可以很簡單地在表達式中使用雙引號:
android:text='@{map["firstName"]}'
也可以用雙引號將屬性包起來。這樣的話,字符串常量就可以用 " 或者反引號 ( ` ) 來調用
android:text="@{map[`firstName`}"
android:text="@{map["firstName"]}"
資源
也可以在表達式中使用普通的語法來引用資源:
android:text="@{@string/fullname(user.fullName)"
字符串格式化和復數形式可以這樣實現:
android:text="@{@plurals/sample_plurals(num)}"
當復數形式有多個參數時,應該這樣寫:
android:text="@{@plurals/numbers(num, num)}"
一些資源需要顯示類型調用。
Type | Normal Reference | Expression Reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
數據對象 (Data Objects)
任何 POJO 對象都能用在 Data Binding 中,但是更改 POJO 并不會同步更新 UI。Data Binding 的強大之處就在于它可以讓你的數據擁有更新通知的能力。
有三種不同的動態更新數據的機制:
- Observable 對象
- Observable 字段
- Observable 容器類
當以上的 observable 對象綁定在 UI 上,數據發生變化時,UI 就會同步更新。
Observable 對象
當一個類實現了 Observable 接口時,Data Binding 會設置一個 listener 在綁定的對象上,以便監聽對象字段的變動。
Observable 接口有一個添加/移除 listener 的機制,但通知取決于開發者。為了簡化開發,Android 原生提供了一個基類 BaseObservable
來實現 listener 注冊機制。這個類也實現了字段變動的通知,只需要在 getter 上使用 Bindable 注解,并在 setter 中通知更新即可。
public class ObservableContact extends BaseObservable {
private String mName;
private String mPhone;
public ObservableContact(String name, String phone) {
mName = name;
mPhone = phone;
}
@Bindable
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
notifyPropertyChanged(BR.name);
}
@Bindable
public String getPhone() {
return mPhone;
}
public void setPhone(String phone) {
mPhone = phone;
notifyPropertyChanged(BR.phone);
}
}
BR
是編譯階段生成的一個類,功能與 R.java 類似,用 @Bindable 標記過 getter 方法會在 BR 中生成一個 entry。
當數據發生變化時需要調用 notifyPropertyChanged(BR.firstName)
通知系統 BR.firstName
這個 entry 的數據已經發生變化以更新UI。
ObservableFields
創建 Observable 類還是需要花費一點時間的,如果想要省時,或者數據類的字段很少的話,可以使用 ObservableField
以及它的派生 ObservableBoolean
、
ObservableByte
、ObservableChar
、ObservableShort
、ObservableInt
、ObservableLong
、ObservableFloat
、ObservableDouble
、
ObservableParcelable
。
ObservableFields 是包含 observable 對象的單一字段。原始版本避免了在存取過程中做打包/解包操作。要使用它,在數據類中創建一個 public final 字段:
public class ObservableFieldContact {
public ObservableField<String> mName = new ObservableField<>();
public ObservableField<String> mPhone = new ObservableField<>();
public ObservableFieldContact(String name, String phone) {
mName.set(name);
mPhone.set(phone);
}
}
要存取數據,只需要使用 get() / set() 方法:
mObservableFieldContact.mName.set("ConnorLin");
mObservableFieldContact.mPhone.set("12345678901");
String name = mObservableFieldContact.mName.get();
Observable Collections 容器類
一些應用會使用更加靈活的結構來保持數據。Observable 容器類允許使用 key 來獲取這類數據。當 key 是類似 String 的一類引用類型時,使用 ObservableArrayMap 會非常方便。
ObservableArrayMap<String, String> mUser = new ObservableArrayMap<>();
mUser.put("firstName", "Connor");
mUser.put("lastName", "Lin");
mUser.put("age", "28");
mBinding.setUser(mUser);
在布局中,可以用 String key 來獲取 map 中的數據:
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, String>"/>
</data>
…
<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"]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user["age"]}'/>
當 key 是整數類型時,可以使用 ObservableArrayList :
ObservableArrayList<String> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add("17");
在布局文件中,使用下標獲取列表數據:
<data>
<import type="android.databinding.ObservableList"/>
<variable name="user" type="ObservableList<String>"/>
</data>
…
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{userList[0]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{userList[1]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{userList[2]}'/>
生成綁定
生成的 binding 類將布局中的 View 與變量綁定在一起。就像先前提到過的,類名和包名可以自定義 。生成的 binding 類會繼承 ViewDataBinding 。
Creating
binding 應該在 inflate 之后創建,確保 View 的層次結構不會在綁定前被干擾。綁定布局有好幾種方式。最常見的是使用 binding 類中的靜態方法。inflate 函數會 inflate View 并將 View 綁定到 binding 類上。此外有更加簡單的函數,只需要一個 LayoutInflater 或一個 ViewGroup:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果布局使用不同的機制來 inflate,則可以獨立做綁定操作:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有時綁定關系是不能提前確定的。這種情況下,可以使用 DataBindingUtil :
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
Views With IDs
布局中每一個帶有 ID 的 View,都會生成一個 public final 字段。binding 過程會做一個簡單的賦值,在 binding 類中保存對應 ID 的 View。這種機制相比調用 findViewById 效率更高。舉個例子:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.connorlin.databinding.model.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>
將會在 binding 類內生成:
public final TextView firstName;
public final TextView lastName;
ID 在 Data Binding 中并不是必需的,但是在某些情況下還是有必要對 View 進行操作。
Variables
每一個變量會有相應的存取函數:
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.connorlin.databinding.model.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
并在 binding 類中生成對應的 getters 和 setters:
public com.connorlin.databinding.model.User getUser();
public void setUser(com.connorlin.databinding.model.User user);
public Drawable getImage();
public void setImage(Drawable image);
public String getNote();
public void setNote(String note);
ViewStubs
本部分源碼請參考 DataBindingDemo -> ViewStubActivity
部分。
ViewStub 相比普通 View 有一些不同。ViewStub 一開始是不可見的,當它們被設置為可見,或者調用 inflate 方法時,ViewStub 會被替換成另外一個布局。
因為 ViewStub 實際上不存在于 View 結構中,binding 類中的類也得移除掉,以便系統回收。因為 binding 類中的 View 都是 final 的,所以Android 提供了一個叫 ViewStubProxy
的類來代替 ViewStub 。開發者可以使用它來操作 ViewStub,獲取 ViewStub inflate 時得到的視圖。
但 inflate 一個新的布局時,必須為新的布局創建一個 binding。因此, ViewStubProxy
必須監聽 ViewStub 的 ViewStub.OnInflateListener
,并及時建立 binding。由于 ViewStub 只能有一個 OnInflateListener
,你可以將你自己的 listener 設置在 ViewStubProxy 上,在 binding 建立之后, listener 就會被觸發。
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout ...>
<ViewStub
android:id="@+id/view_stub"
android:layout="@layout/include"
... />
</LinearLayout>
</layout>
在 Java 代碼中獲取 binding 實例,為 ViewStubProy
注冊 ViewStub.OnInflateListener
事件:
mActivityViewStubBinding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
mActivityViewStubBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
IncludeBinding viewStubBinding = DataBindingUtil.bind(inflated);
User user = new User("Connor", "Lin", 28);
viewStubBinding.setUser(user);
}
});
通過 ViewStubProxy
來 inflate ViewStub :
public void inflate(View view) {
if (!mActivityViewStubBinding.viewStub.isInflated()) {
mActivityViewStubBinding.viewStub.getViewStub().inflate();
}
}
此處
isInflated()
和getViewStub()
會標紅,請不要擔心,這并不是錯誤,是 ViewStubProxy 中的方法。
高級綁定
動態變量
有時候,有一些不可知的 binding 類。例如,RecyclerView.Adapter 可以用來處理不同布局,這樣的話它就不知道應該使用哪一個 binding 類。而在 onBindViewHolder(VH, int) ) 的時候,binding 類必須被賦值。
在這種情況下,RecyclerView 的布局內置了一個 item 變量。 BindingHolder 有一個 getBinding 方法,返回一個 ViewDataBinding 基類。
public void onBindViewHolder(BindingHolder holder, int position) {
holder.getBinding().setVariable(BR.item, mItemList.get(position));
holder.getBinding().executePendingBindings();
}
以上,詳細請參考 DataBindingDemo -> MainActivity
部分(使用 RecyclerView
實現)。
直接 binding
當變量或者 observable 發生變動時,會在下一幀觸發 binding。有時候 binding 需要馬上執行,這時候可以使用 executePendingBindings()
。
后臺線程
只要數據不是容器類,你可以直接在后臺線程做數據變動。Data binding 會將變量/字段轉為局部量,避免同步問題。
屬性設置
本部分源碼請參考 DataBindingDemo -> AttributeSettersActivity
部分。
當綁定數據發生變動時,生成的 binding 類必須根據 binding 表達式調用 View 的 setter 函數。Data binding 框架內置了幾種自定義賦值的方法。
自動設置屬性
對一個 attribute 來說,Data Binding 會嘗試尋找對應的 setAttribute 函數。屬性的命名空間不會對這個過程產生影響,只有屬性的命名才是決定因素。
舉個例子,針對一個與 TextView 的 android:text 綁定的表達式,Data Binding會自動尋找 setText(String) 函數。如果表達式返回值為 int 類型, Data Binding則會尋找 setText(int) 函數。所以需要小心處理函數的返回值類型,必要的時候使用強制類型轉換。
需要注意的是,Data Binding 在對應名稱的屬性不存在的時候也能繼續工作。你可以輕而易舉地使用 Data Binding 為任何 setter “創建” 屬性。
如 DataBindingDemo 中的自定義布局 Card,并沒有添加 declare-styleable
,但是可以使用自動 setter 的特性來調用這些函數。
<com.connorlin.databinding.view.Card
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:object="@{user}"/>
重命名屬性設置
一些屬性的命名與 setter 不對應。針對這些函數,可以用 BindingMethods 注解來將屬性與 setter 綁定在一起。舉個例子, android:tint
屬性可以這樣與 setImageTintList(ColorStateList) )
綁定,而不是 setTint :
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
Android 框架中的 setter 重命名已經在庫中實現了,我們只需要專注于自己的 setter。
自定義屬性設置
一些屬性需要自定義 setter 邏輯。例如,目前沒有與 android:paddingLeft
對應的 setter,只有一個 setPadding(left, top, right, bottom)
函數。結合靜態 binding adapter 函數與 BindingAdapter 注解可以讓開發者自定義屬性 setter。
Android 屬性已經內置一些 BindingAdapter。例如,這是一個 paddingLeft 的自定義 setter:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding adapter 在其他自定義類型上也很好用。舉個例子,一個 loader 可以在非主線程加載圖片。
當存在沖突時,開發者創建的 binding adapter 會覆蓋 Data Binding 的默認 adapter。
你也可以創建多個參數的 adapter:
// 無需手動調用此函數
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Glide.with(view.getContext()).load(url).error(error).into(view);
}
<!-- 當url存在時,會自動調用注解方法,即loadImage()-->
<ImageView
app:imageUrl=“@{url}”
app:error=“@{@drawable/ic_launcher}”/>
當 imageUrl 與 error 存在時這個 adapter 會被調用。imageUrl 是一個 String,error 是一個 Drawable。
- 在匹配時自定義命名空間會被忽略
- 你可以為 android 命名空間編寫 adapter
Binding adapter 方法可以獲取舊的賦值。只需要將舊值放置在前,新值放置在后:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
事件 handlers 僅可用于只擁有一個抽象方法的接口或者抽象類。例如:
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
當 listener 內置多個函數時,必須分割成多個 listener。例如, View.OnAttachStateChangeListener 內置兩個函數: onViewAttachedToWindow()
與 onViewDetachedFromWindow()
。在這里必須為兩個不同的屬性創建不同的接口。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因為改變一個 listener 會影響到另外一個,我們必須編寫三個不同的 adapter,包括修改一個屬性的和修改兩個屬性的。
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
上面的例子比普通情況下復雜,因為 View 是 add/remove
View.OnAttachStateChangeListener
而不是 set
。 android.databinding.adapters.ListenerUtil
可以用來輔助跟蹤舊的 listener 并移除它。
對應 addOnAttachStateChangeListener(View.OnAttachStateChangeListener) )
支持的 api 版本,
通過向 OnViewDetachedFromWindow
和 OnViewAttachedToWindow
添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1)
注解,
Data Binding 代碼生成器會知道這些 listener 只會在 Honeycomb MR1 或更新的設備上使用。
轉換器Converters
對象轉換
當 binding 表達式返回對象時,會選擇一個 setter(自動 Setter,重命名 Setter,自定義 Setter),將返回對象強制轉換成 setter 需要的類型。
下面是一個使用 ObservableMap 保存數據的例子:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在這里, userMap 會返回 Object 類型的值,而返回值會被自動轉換成 setText(CharSequence) 需要的類型。當對參數類型存在疑惑時,開發者需要手動做類型轉換。
自定義轉換
有時候會自動在特定類型直接做類型轉換。例如,當設置背景的時候:
<View
android:background="@{isError.get() ? @color/colorAccent : @color/colorPrimary}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在這里,背景需要的是 Drawable ,但是 color 是一個整數。當需要 Drawable 卻返回了一個整數時, int 會自動轉換成 ColorDrawable 。這個轉換是在一個 BindingConversation 注解的靜態函數中實現:
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
需要注意的是,這個轉換只能在 setter 階段生效,所以 不允許 混合類型:
<View
android:background="@{isError.get() ? @drawable/error : @color/colorPrimary}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Android Studio對Data Binding的支持
-
Android Studio 支持 Data Binding 表現為:
- 語法高亮
- 標記表達式語法錯誤
- XML 代碼補全
- 跳轉到聲明或快速文檔
注意:數組和泛型類型,如 Observable 類,當沒有錯誤時可能會顯示錯誤。
- 在預覽窗口可顯示 Data Binding 表達式的默認值。例如:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=FirstName}"/>
<!-- TextView 的 text 默認值為 FirstName -->
如果你需要在設計階段顯示默認值,你可以使用 tools
屬性代替默認值表達式,詳見 設計階段布局屬性
參考資料
我的簡書賬號是 ConnorLin,歡迎關注!
我的簡書專題是 Android開發技術分享,歡迎關注!