前言
想學mvvm databinding是繞不開的坎,這篇blog,來自官網翻譯,不喜勿噴。
本文檔介紹了如何使用資料綁定庫編寫聲明性布局并最大限度地減少綁定應用程式邏輯和布局所需的代碼
建構環境
資料綁定庫提供了靈活性和廣泛的相容性 - 它是一個支援庫,是以您可以将其與所有Android平台版本一起使用 ,回到Android 2.1(API級别為7+)
要使用資料綁定,需要Android Plugin for Gradle 1.5.0-alpha1或更高版本。
請從Android SDK管理器的支援資訊庫中下載下傳庫。
使用以下代碼段來配置資料綁定
android {
....
dataBinding {
enabled = true
}
}
資料綁定布局檔案
寫你的第一組資料綁定表達式
資料綁定布局檔案略有不同,從布局的根标記開始,後跟資料元素和視圖根元素。
<?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>
内的user變量描述了可在此布局中使用的屬性。
<variable name="user" type="com.example.User"/>
布局中的表達式使用“@ { }”文法寫入屬性。例如,TextView的文本設定為user的firstName屬性:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
資料對象
假設你有一個簡單的Java對象 User
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
在應用程式中通常會讀取一次資料,從此不會更改。也可以使用JavaBeans對象:
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;
}
}
資料綁定的角度來看,這兩個類是等價的。資料綁定将通路前一類中的firstName字段和後一類中的getFirstName()方法。将被解析為firstName()
資料綁定
預設情況下,将根據布局檔案的名稱生成一個Binding類,将其轉換為駝峰式命名并字尾“Binding”,上面的布局檔案是main_activity.xml,是以生成類是MainActivityBinding
建立綁定的最簡單方法是在inflating時執行此操作:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
運作應用程式,您将在UI中看到Test。或者,您可以通過以下方式擷取視圖:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果您正在ListView或RecyclerView.adatper中使用資料綁定項,則可能更喜歡使用
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
進行簡單的回顧
建構環境、布局檔案定義類型、資料綁定生成檔案名以及布局填充幾種方式。
事件處理
要将事件配置設定給其處理程式,請使用正常的綁定表達式,其值為要調用的方法名稱。例如,如果您的資料對象有兩種方法:
public class MyHandlers {
public void onClickFriend(View view) { ... }
public void onClickEnemy(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.Handlers"/>
<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="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
</LinearLayout>
</layout>
布局細節
import 用法
可以在元素内部使用零個或多個導入元素。這些可以輕松地引用布局檔案中的類,就像Java一樣。
<data>
<import type="android.view.View"/>
</data>
現在,View可能在你的綁定表達式中使用:
<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"/>
現在,Vista可能被用來引用com.example.real.estate.View和View可以用來引用布局檔案中的android.view.View。導入的類型可以用作變量和表達式中的類型引用:
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
Android Studio尚未處理導入,是以導入的變量的自動完成可能無法在IDE中運作。但是您的應用程式仍然可以正常編譯,您可以通過在變量定義中使用完全限定名稱來解決IDE問題。
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在引用表達式中的靜态字段和方法時也可以使用導入的類型:
<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"/>
就像Java一樣,java.lang。*是自動導入的。
變量
元素内可以使用任意數量的可變元素,每個變量元素描述可以在布局中設定的屬性,供布局檔案中綁定表達式中使用。
<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 *接口的基類或接口,則不會觀察到變量!當多種配置(例如橫向或縱向)有不同的布局檔案時,将組合變量。這些布局檔案之間不能有沖突的變量定義。
引用類型為null,int為int,布爾值為false等。
自定義綁定類名
預設情況下,Binding類是根據布局檔案的名稱生成的,以大寫形式開始,删除下劃線(_),并大寫以下字母,然後字尾為“Binding”。
該類将被放置在子產品包下的資料綁定包中,例如,布局檔案contact_item.xml将生成ContactItemBinding。如果子產品包是com.example.my.app,那麼它将被放在com.example.my.app.databinding中通過調整資料元素的類屬性将綁定類重命名或放置在不同的包中。
<data class="ContactItem">
...
</data>
将在子產品包中的資料綁定包中生成綁定類作為ContactItem。如果該類應該在子產品包中的不同包中生成,則可以使用“.”作為字首
<data class=".ContactItem">
...
</data>
ContactItem是直接在子產品包中生成的。如果提供完整的包裝,會在具體的包中生成:
<data class="com.example.ContactItem">
...
</data>
Includes
變量可以通過使用應用程式命名空間和屬性中的變量名稱從包含的布局傳遞到包含的布局的綁定中
<?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>
在這裡,name.xml和contact.xml布局檔案中都必須有一個使用者變量。
資料綁定不支援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.example.User"/>
</data>
<merge>
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>
表達式語言
Mathematical + - / * %
String concatenation +
Logical && ||
Binary & | ^
Unary + - ! ~
Shift >> >>> <<
Comparison == > < >= <=
instanceof
Grouping ()
Literals - character, String, numeric, null
Cast
Method calls
Field access
Array access []
Ternary operator ?:
例如:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
缺少操作
this
super
new
顯示的泛型調用
新的操作符号
空合并運算符(??)選擇左操作數,如果它不為空,或者如果為空,則選擇右操作數。
android:text="@{user.displayName ?? user.lastName}"
功能上相當于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
避免了NullPointerException
生成的資料綁定代碼自動檢查null并避免空指針異常。例如,在表達式@ {user.name}。,如果user為空,user.name将被配置設定其預設值(null)。如果你引用user.age,其中age是一個int,那麼它将預設為0。
集合
數組,清單, sparseLists和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]}"
引号的使用
在屬性值周圍使用單引号時,在表達式中很容易使用雙引号:
android:text='@{map["firstName"]}'
也可以使用雙引号來包圍屬性值
android:text="@{map[`firstName`}"
android:text="@{map["firstName"]}"
Resources
使用正常文法可以通路資源作為表達式的一部分
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)}"
一些資源需要顯示的評估
資料對象
任何普通的Java對象(POJO)可能用于資料綁定,但修改POJO不會導緻UI更新
這裡面提供三種更新方式。
Observable objects,
observable fields,
observable collections.
當這些可觀察的資料對象之一被綁定到UI并且資料對象的屬性改變時,UI将被自動更新。
觀察對象
實作Observable接口的類将允許綁定将單個偵聽器附加到綁定對象以監聽該對象上所有屬性的更改
他通過将一個Bindable注釋配置設定給getter并在setter中通知來完成。
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
Bindable注釋在編譯期間在BR類檔案中生成一個條目。 BR類檔案将在子產品包中生成
Observable Fields
上面需要一些少量的工作,節省時間或者少量屬性的開發人員可能會使用ObservableField 及兄弟類
ObservableBoolean, ObservableByte, ObservableChar,ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble,ObservableParcelable
原始版本在通路操作期間避免了裝箱和拆箱。要使用,在資料類中建立一個公共final字段
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
設定和通路
user.firstName.set("Google");
int age = user.age.get();
觀察集合
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局中,可以通過String鍵通路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<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在布局中,可以通過索引通路清單:
<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"/>
生成的綁定
生成的綁定類将布局變量與布局中的視圖相連結。生成bind類擴充自
ViewDataBinding
建立
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
-----
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
Sometimes the binding cannot be known in advance. In such cases, the binding can be created using the DataBindingUtil class:
------
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
View 辨別
綁定在View層次結構上執行單次傳遞,使用ID提取視圖。這個機制可以比為幾個視圖調用findViewById更快。例如:
<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>
public final TextView firstName;
public final TextView lastName;
變量
每一個變量将被賦予通路器方法
<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>
生成
public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);
ViewStubs
ViewStubs與普通視圖略有不同。他們從隐形開始,當他們被看見或明确地被告知要膨脹時,他們通過膨脹另一個布局來替代自己的布局。
ViewStubProxy 設定 一個監聽,在收到監聽回調的時候進行布局的綁定
進階綁定
立即綁定
有時,不知道具體的綁定類。例如,對任意布局操作的RecyclerView.Adapter将不會知道特定的綁定類。它仍然必須在onBindViewHolder(VH,int)期間配置設定綁定值。
綁定的執行時間
當變量或observable發生變化時,綁定将被安排在下一幀之前更改
背景線程的
屬性 Setters
每當綁定值發生變化時,生成的綁定類必須使用綁定表達式在View上調用setter方法,資料綁定架構有方法來自定義調用哪個方法來設定值。
#### 自動 Setters
對于屬性,資料綁定嘗試查找方法setAttribute。屬性的命名空間并不重要,隻有屬性名稱本身。
例如,與TextView的屬性android:text相關聯的表達式将尋找一個setText(String)。如果表達式傳回一個int,資料綁定将搜尋一個setText(int)方法。
更名 Setters
某些屬性具有與名稱不比對的設定器。對于這些方法,可以通過BindingMethods注釋将屬性與setter相關聯,這必須與一個類相關聯并且包含BindingMethod注釋,android:tint屬性真的與setImageTintList(ColorStateList)相關聯,而不是setTint。
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
開發人員不太可能需要重命名設定器; android架構的屬性已經被實作了。
自定義 Setters
某些屬性需要自定義綁定邏輯。例如,android:paddingLeft屬性沒有關聯的設定器。相反,setPadding(左,上,右,下)存在。具有BindingAdapter注釋的靜态綁定擴充卡方法允許開發人員自定義如何調用屬性的setter,android屬性已經建立了BindingAdapters。
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
綁定擴充卡可用于其他類型的定制。例如,一個自定義加載器可以調用離線加載圖像。
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl=“@{venue.imageUrl}”
app:error=“@{@drawable/venueError}”/>
如果ImageUrl和Error都用于ImageView,并且imageUrl是一個字元串,并且錯誤是可繪制的,則将調用此擴充卡。
自定義命名空間在比對期間被忽略。
您還可以為Android命名空間編寫擴充卡。
綁定擴充卡方法可以選擇在其處理程式中使用舊值。使用舊值和新值的方法應該具有屬性的所有舊值,然後是新值
新指派 + 不變舊的值
@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());
}
}
事件處理程式隻能與一個抽象方法與接口或抽象類一起使用。例如:
@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);
}
}
}
當一個監聽器有多個方法時,它必須被分割成多個監聽器。
例如,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);
}
因為更改一個監聽器也會影響另一個監聽器,是以我們必須有三個不同的綁定擴充卡,每個屬性一個,兩個都有一個。
@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類有助于跟蹤以前的偵聽器,以便它們可以在Binding Adaper中被删除。
通過使用@TargetApi(VERSION_CODES.HONEYCOMB_MR1)注釋接口OnViewDetachedFromWindow和OnViewAttachedToWindow,資料綁定代碼生成器知道,隻有在Honeycomb MR1和新裝置上運作時才應生成偵聽器,與addOnAttachStateChangeListener(View.OnAttachStateChangeListener)支援的版本相同。
轉換器
對象轉換
從綁定表達式傳回對象時,将從自動,重命名和自定義設定器中選擇一個設定器。對象将被轉換為所選設定器的參數類型。
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap傳回一個Object,該對象将被自動轉換為在setter setText(CharSequence)中找到的參數類型。當對參數類型可能有混淆時,開發人員将需要在表達式中轉換。
定制的轉換
有時,特定類型之間的轉換應是自動的。例如,設定背
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
,背景采用Drawable,但是顔色是一個整數。每當有一個Drawable并傳回一個整數時,應将int轉換為ColorDrawable。使用帶有BindingConversion注釋的靜态方法完成此轉換
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>