MVVM 和 Android Data Binding

在 Android 開發過程中,由于 Android 作為 View 描述的 xml 視圖功能較弱,開發中很容易寫出臃腫繁雜的 Activity/Fragment,甚至有寫出過數千行代碼的 Activity。大量的顯示、校驗、事件響應、回調接口充斥在 Activity 中。Activity/Fragment 實際上成了 View 和 Controller 的混合體,既要承擔 View 的顯示功能,又要承擔 Controller 的控制功能。承擔的功能過多,膨脹成繁蕪的巨類也就不足為怪了。

UI 界面設計模式

在傳統的 UI 設計模式中,MVC 已經實踐中證明了其價值,并在漫長的使用過程中((MVC最早出現于 1970 年代)演化出 MVP 和 MVVM 多個變種。

MVC

MVC 我們都知道是 Model-View-Controller,為了使得程序的各個部分分離降低耦合性,MVC 除了把應用程序分成 View、Model 層,還額外的加了一個 Controller 層,它的職責為進行 Model 和 View 之間的協作(路由、輸入預處理等)的應用邏輯;Model 進行處理業務邏輯。

MVC Pattern

MVC 通常的處理時序如下:

  1. View 接受用戶的交互請求;
  2. View 將請求轉交給 Controller;
  3. Controller 操作 Model 進行數據更新;
  4. 數據更新之后,Model 通知 View 數據變化;
  5. View 顯示更新之后的數據。

通常 Model 使用 Observer 模式通知 View 數據變化:

MVC with Observer

MVC 的優點:

  • 把業務邏輯和展示邏輯分離,模塊化程度高。且當應用邏輯需要變更的時候,不需要變更業務邏輯和展示邏輯,只需要Controller換成另外一個Controller就行了(Swappable Controller)。
  • 觀察者模式可以做到多視圖同時更新。

MVD 的缺點:

  • Controller 測試困難。因為視圖同步操作是由 View 自己執行,而 View 只能在 UI 環境下運行。在沒有 UI 環境下對 Controller 進行單元測試的時候,應用邏輯正確性是無法驗證的:Model 更新的時候,無法對 View 的更新操作進行斷言。
  • View 無法組件化。View 是強依賴特定的 Model 的,如果需要把這個 View 抽出來作為一個另外一個應用程序可復用的組件就困難了。因為不同程序的的 Model 是不一樣的。
  • 當有變化的時候需要同時維護 Model, View, Controller 及其交互,這顯然讓事情復雜化了。

MVP

為了解決 MVC 的權限,MVP 對 MVC 進行了改良,MVP 模式把 MVC 模式中的 Controller 換成了 Presenter:

MVP Pattern

MVP 通常的調用時序如下:

  1. View 接受用戶的交互請求;
  2. View 將請求轉交給 Presenter;
  3. Presenter 操作 Model 進行業務處理;
  4. Model 通知 Presenter 數據發生變化;
  5. Presenter 更新 View 的數據。

和 MVC 不同的是,Presenter 會反作用于 View,不像 Controller 只能被動的接受 View 的指揮。

通常我們會抽象 View 接口,暴露屬性和事件,然后 Presenter 引用 View 接口。這樣可以很容易的構造 View 的 Mock 對象,提高可單元測試性。在這里,Presenter 的責任變大了,不僅要操作數據,而且要更新 View。

上面講的是 MVP 的 Passive View 模式,該模式下 View 非常 Passive,它幾乎什么都不知道,Presenter 讓它干什么它就干什么。

在實際的實現中,有人會傾向于 獎 Presenter 一部分簡單的同步邏輯交給 View 自己去做,Presenter 只負責比較復雜的、高層次的 UI 操作,所以可以把它看成一個 Supervising Controller,這種模式也被稱為 The Supervising Controller MVP:

The Supervising Controller MVP

MVP 的優點:

  • 便于測試。Presenter 對 View 是通過接口進行,在對 Presenter 進行不依賴 UI 環境的單元測試的時候。可以通過 Mock 一個 View 對象,這個對象只需要實現了 View 的接口即可。
  • View 可以進行組件化。在 MVP 當中,View 不依賴 Model。這樣就可以讓 View 從特定的業務場景中脫離出來,可以說 View 可以做到對業務完全無知。它只需要提供一系列接口提供給上層操作。這樣就可以做到高度可復用的 View組件。

MVP 缺點:

  • Presenter 中除了應用邏輯以外,還有大量的 View->Model,Model->View 的手動同步邏輯,造成 Presenter 比較笨重,維護起來會比較困難。

MVVM

MVVM 可以看作是一種特殊的 MVP(Passive View)模式,或者說是對 MVP 模式的進一步改良。

MVVM 模式最早是微軟公司提出,并且了大量使用在.NET的WPF和Sliverlight中。2005年微軟工程師John Gossman在自己的博客上首次公布了MVVM模式。

MVVM Pattern

MVVM 代表的是 Model-View-ViewModel。MVVM 模式將 Presenter 改名為 ViewModel,基本上與 MVP 模式完全一致,唯一區別在于 ViewModel 將密切關聯的 Model 和 View 的邏輯單獨提取出來,用數據綁定將他們關聯到一起。Model 的改變會通過 ViewModel 來映射到 View 上,反之亦然。數據綁定你可以認為是 Observer 模式或者是 Publish/Subscribe 模式,原理都是為了用一種統一的集中的方式實現頻繁需要被實現的數據更新問題。

比起MVP,MVVM 不僅簡化了業務與界面的依賴關系,還優化了數據頻繁更新的解決方案,甚至可以說提供了一種有效的解決模式。

MVVM 優點:

  • 省去了model變化之后手動修改view和view變化之后手動修改model的繁瑣工作;
  • UI和功能更加松耦合了,功能的可測試性就越來越強。

MVVM 缺點:

  • 在復雜的情況下,很難預先設計好足夠通用的 ViewModel;
  • 通常會依賴于特定的數據綁定框架;

Android Data Binding

2015 Google IO 大會帶來的 Data Binding 庫使得 Android 開發者可以方便的實現 MVVM 架構模式。

警告:Data Binding 庫目前還是 Beta 版本,采用需謹慎。


以下大部分內容摘錄自Data Binding(數據綁定)用戶指南,根據 Android 官方英文指南 做了一些更新。

配置環境

最新版的 Android Studio 已經內置了對 Android Data Binding 框架的支持,配置起來也很簡單,只需要在 app 的 build.gradle 文件中添加下面的內容就好了

android {
    ....
    dataBinding {
        enabled = true
    }
}

Data Binding Layout 文件

Data Binding 表達式

Data Binding layout 文件有點不同的是:起始根標簽是 layout,接下來一個 data 元素以及一個 view 的根元素。這個 view 元素就是你沒有使用 Data Binding的layout文件的根元素。舉例說明如下:

<?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的變量屬性,使其可以在這個layout中使用:

<variable name="user" type="com.example.User"/>

在layout的屬性表達式寫作 @{},下面是一個 TextView 的 text 設置為 user 的 firstName 屬性:

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>

Data Object

假設你有一個user的plain-old Java Object(POJO):

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}

這個類型的對象擁有從不改變的數據。在 app 中它是常見的,可以讀取一次并且之后從不改變。當然也可以使用 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;
   }
}

從 Data Binding 的角度來看,這兩個類是等價的。用于 TextView 中的 android:text 屬性的表達式 @{user.firstName} 將訪問前者 POJO 對象中的 firstName 和后者 JavaBeans 對象中的 getFirstName() 方法。

綁定數據

默認情況下,一個 Binding 類會基于 layout 文件的名稱而產生,將其轉換為 Pascal case(譯注:首字母大寫的命名規范)并且添加 “Binding” 后綴。上述的 layout 文件是 activity_main.xml,因此生成的類名是 ActivityMainBinding。此類包含從 layout 屬性到 layout 的 Views 中所有的 bindings(例如user變量),并且它還知道如何給 Binding 表達式分配數值。創建 bindings 的最簡單的方式是在 inflating(譯注:layout文件與Activity/Fragment的“鏈接”)期間如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
}

就是這樣,運行 app 后,你將會看到 Test User。或者你可以通過如下獲取 View:

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());

如果你在 ListView 或者 RecyclerView adapter 使用 Data Binding 時,你可能會使用:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup,
false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

綁定事件

就像你可以在xml文件里面使用屬性android:onClick綁定Activity里面的一個方法一樣,Data Binding Library 擴展了更多的事件可以用來綁定方法,比如 View.OnLongClickListener 有個方法 onLongClick(), 你就可以使用 android:onLongClick 屬性來綁定一個方法,需要注意的是綁定的方法的簽名必須和該屬性原本對應的方法的簽名完全一樣,否則編譯階段會報錯。

下面舉例來說明具體怎么使用,先看用來綁定事件的類:

public class MyHandlers {
    public void onClickButton(View view) { ... }

    public void afterFirstNameChanged(Editable s) { ... }
}

然后就是layout文件:

<?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">
        <EditText android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}"
            android:afterTextChanged="@{handlers.afterFirstNameChanged}"/>
        <Button android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{handlers.onClickButton}"/>
    </LinearLayout></layout>

深入 Layout 文件

Import

零個或多個 import 元素可能在 data 元素中使用。這些只用在你的 layout 文件中添加引用,就像在 Java 中:

<data>
    <import type="android.view.View"/>
</data>

現在,View 可以使用你的 Binding 表達式:

<TextView
   android:text="@{user.lastName}"
   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.example.real.estate.View"
        alias="Vista"/>

這樣,在該 layout 文件中 Vista 對應 com.example.real.estate.View,而View對應android.view.View。導入的類型可以在Variable和表達式中使用作為引用來使用:

<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還沒有處理imports,所以自動導入Variable在你的IDE不能使用。您的app仍會正常編譯,你可以在您的Variable定義中使用完全符合規定的名稱來解決該IDE問題。

<TextView
   android:text="@{((User)(user.connection)).lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

導入的類型還可以在表達式中使用 static 屬性和方法:

<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。*是自動導入的。

Variables

在data中可以使用任意數量的variable元素。每一個variable元素描述了一個用于layout文件中Binding表達式的屬性。

<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>

該Variable類型在編譯時檢查,因此如果一個Variable實現了Observable或observable collection,這應該反映在類型中。(譯注:需要查找資料來理解)如果variable是一個沒有實現Observable接口的基本類或者接口,Variables不會被observed!

當對于多種配置有不同的layout文件時(如,橫向或縱向),Variables會被合并。這些layout文件之間必須不能有沖突的Variable定義。

產生的Binding類對于每一個描述的Variables都會有setter和getter。這些Variables會使用默認的Java值 - null(引用類型)、0(int)、false(boolean)等等,直到調用setter時。

自定義 Binding 類名稱

默認情況下,Binding類的命名是基于所述layout文件的名稱,用大寫開頭,除去下劃線()以及()后的第一個字母大寫,然后添加“Binding”后綴。這個類將被放置在一個模塊封裝包里的databinding封裝包下。例如,所述layout文件contact_item.xml將生成ContactItemBinding。如果模塊包是com.example.my.app,那么它將被放置在com.example.my.app.databinding。

Binding類可通過調整data元素中的class屬性來重命名或放置在不同的包中。例如:

<data class="ContactItem">
    ...
</data>

在模塊封裝包的databinding包中會生成名為ContactItem的Binding類。如果要想讓該類生成在不同的包種,你需要添加前綴.,如下:

<data class=".ContactItem">
    ...
</data>

在這個情況下,ContactItem類直接在模塊包種生成。或者你可以提供整個包名:

<data class="com.example.ContactItem">
    ...
</data>

Includes

通過使用application namespace以及在屬性中的Variable名字從容器layout中傳遞Variables到一個被包含的layout:

<?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兩個layout文件中必需要有user variable

Data binding 不支持包含 merge 元素作為直接的子元素,比如以下layout是不支持的:

<?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>

表達式語言

常用表達式跟Java表達式很像,以下這些是一樣的:

  • 數學表達式 + – / * %
  • 字符串鏈接 +
  • 邏輯操作符 && ||
  • 二元操作符 & | ^
  • 一元操作符 + – ! ~
  • Shift >> >>> <<
  • 比較 == > < >= <=
  • instanceof
  • Grouping ()
  • Literals – character, String, numeric, null
  • Cast
  • 函數調用
  • 值域引用(Field access)
  • 通過[]訪問數組里面的對象
  • 三元操作符 ?:

示例:

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}"

屬性引用

我們已經在前邊“Data Binding表達式”中提到了JavaBean引用的簡短格式。

當一個表達式引用一個類的屬性,它仍使用同樣的格式對于字段、getters以及ObservableFields。

android:text="@{user.lastName}"

避免 NullPointerException

Data Binding代碼生成時自動檢查是否為nulls來避免出現null pointer exceptions錯誤。例如,在表達式@{user.name}中,如果user是null,user.name會賦予它的默認值(null)。如果你引用user.age,age是int類型,那么它的默認值是0。

集合

常用的集合:arrays、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"]}"

Resources

使用正常的表達式來訪問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)}"

一些資源需要顯式類型判斷:

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 Object

任何Plain old Java object(PO??JO)可用于Data Binding,但修改POJO不會導致UI更新。Data Binding的真正能力是當數據變化時,可以通知給你的Data對象。有三種不同的數據變化通知機制:Observable對象、ObservableFields以及observable collections。

當這些可觀察Data對象??綁定到UI,Data對象屬性的更改后,UI也將自動更新。

Observable 對象

實現android.databinding.Observable接口的類可以允許附加一個監聽器到Bound對象以便監聽對象上的所有屬性的變化。

Observable接口有一個機制來添加和刪除監聽器,但通知與否由開發人員管理。為了使開發更容易,一個BaseObservable的基類為實現監聽器注冊機制而創建。Data實現類依然負責通知當屬性改變時。這是通過指定一個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 getFirstName() {
       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類文件中生成一個Entry。BR類文件會在模塊包內生成。如果用于Data類的基類不能改變,Observable接口通過方便的PropertyChangeRegistry來實現用于儲存和有效地通知監聽器。

Observable 字段

一些小工作會涉及到創建Observable類,因此那些想要節省時間或者幾乎沒有幾個屬性的開發者可以使用ObservableFields。ObservableFields是自包含具有單個字段的observable對象。它有所有基本類型和一個是引用類型。要使用它需要在data對象中創建public final字段:

private static class User extends BaseObservable {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

就是這樣,要訪問該值,使用set和get方法:

user.firstName.set("Google");
int age = user.age.get();

在實踐過程中,有時 notifyPropertyChanged(BR.lastName); 很容易引用錯誤,因此,開發過程中還是推薦使用 ObservableField。

Observable 集合

一些app使用更多的動態結構來保存數據。Observable集合允許鍵控訪問這些data對象。ObservableArrayMap用于鍵是引用類型,如String。

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在layout文件中,通過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);

在layout文件中,通過索引可以訪問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"/>

生成 Binding

Binding類的生成鏈接了layout中variables與Views。如前所述,Binding的名稱和包名可以定制。所生成的Binding類都擴展了android.databinding.ViewDataBinding。

創建

Binding應在inflation之后就立馬創建,以確保View層次結構不在之前打擾layout中的binding到views上的表達式。有幾個方法可以綁定到一個layout。最常見的是在Binding類上使用靜態方法.inflate方法載入View的層次結構并且綁定到它只需這一步。還有一個更簡單的版本,只需要LayoutInflater還有一個是采用ViewGroup:

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(LayoutInflater, viewGroup, false);

如果使用不同的機制載入layout,他可一分開綁定:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有時Binding不能提前知道,對于這種情況,可以使用DataBindingUtil類來創建Binding:

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

帶ID的Views

在layout中對于每個帶ID的View會生成一個public final字段。Binding在View層次結構上做單一的傳遞,提取帶ID的Views。這種機制比起某些Views使用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>

它會生成如下的Binding類:

public final TextView firstName;
public final TextView lastName;

IDs不像沒有Data Bindings那樣幾乎沒有必要,但是仍然會有一些實例需要從代碼中訪問Views。

Variables

每個Variable會有訪問方法。

<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>

它會在Binding中生成setters和getters:

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);

Binding進階

動態Variables

有時,不知道具體的Binding類,例如,一個RecyclerView適配器對layouts任意操作并不知道具體的Binding類。它仍然必需在onBindViewHolder期間賦值給Binding。

在這個例子中,該RecyclerView綁定的所有layouts有一個“item”的Variable。該BindingHolder有一個getBinding方法返回ViewDataBinding。

public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

直接Binding

當一個variable或observable變化時,binding會在計劃在下一幀之前執行改變。可能會發生很多次,但是在Binding時必須立即執行。要強制執行,使用executePendingBindings()方法。

后臺線程

只要它不是一個集合,你可以在后臺線程中改變你的數據模型。在判斷是否要避免任何并發問題時,Data Binding會對每個Varialbe/field本地化。

屬性Setters

每當綁定值的變化,生成的Binding類必須調用setter方法。Data Binding 框架有可以自定義賦值的方法。

自動Setters

對于一個屬性,Data Binding試圖找到setAttribute方法。與該屬性的namespace并不什么關系,僅僅與屬性本身名稱有關。

例如,有關TextView的android:text屬性的表達式會尋找一個setText(String)的方法。如果表達式返回一個int,Data Binding會搜索的setText(int)方法。注意:要表達式返回正確的類型,如果需要的話使用casting。Data Binding仍會工作即使沒有給定名稱的屬性存在。然后,您可以通過Data Binding輕松地為任何setter“創造”屬性。例如,DrawerLayout沒有任何屬性,但有大量的setters。您可以使用自動setters來使用其中的一個。

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

重命名的Setters

一些有setters的屬性按名稱并不匹配。對于這些方法,屬性可以通過BindingMethods注解相關聯。這必須與一個包含BindingMethod注解的類相關聯,每一個用于一個重命名的方法。例如,android:tint屬性與setImageTintList相關聯,而不與setTint相關。

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

以上例子,開發者需要重命名setters是不太可能了,android架構屬性已經實現了。

自定義Setters

有些屬性需要自定義綁定邏輯。例如,對于android:paddingLeft屬性并沒有相關setter。相反,setPadding(left, top, right, bottom)是存在在。一個帶有BindingAdapter注解的靜態綁定適配器方法允許開發者自定義setter如何對于一個屬性的調用。

Android的屬性已經創造了BindingAdapters。舉例來說,對于paddingLeft:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

Binding適配器對其他定制類型非常有用。例如,自定義loader可以用來異步載入圖像。

當有沖突時,開發人員創建的Binding適配器將覆蓋Data Binding默認適配器。

您也可以創建可以接收多個參數的適配器。

@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}”/>

如果對于一個ImageViewimageUrl和error都被使用并且imageUrl是一個string類型以及error是一個drawable時,該適配器會被調用。

  • 匹配的過程中自定義namespaces將被忽略。
  • 你也可以為Android namespaces寫適配器。

Binding適配器方法可能從handlers中獲取舊的屬性值. 同時獲取新舊屬性值的方法應該把舊的屬性值作為參數放在前邊,緊跟著是新的屬性值:

@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);
}

因為改變某個監聽器會影響到其他的監聽器,我們必須編碼三個不同的binding適配器,為每個屬性各編寫一個,并同時為兩者一起編寫一個,他們必須同時被設置。

@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 類保持對之前所有監聽器的追蹤,所以,他們必須從綁定適配器中移除。

通過對接口 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 用 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 進行注解, 數據綁定代碼生成器明白監聽器僅需在運行 Honeycomb 及以上版本的設備上生成,
addOnAttachStateChangeListener(View.OnAttachStateChangeListener) 支持同樣的版本.

轉換

對象轉換

當從Binding表達式返回一個對象,一個setter會從自動、重命名以及自定義的setters中選擇。該對象將被轉換為所選擇的setter的參數類型。

這是為了方便那些使用ObservableMaps來保存數據。例如:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

在userMap返回一個對象并且該對象將自動轉換為setText(CharSequence)的參數類型。當有關參數類型可能混亂時,開發人員需要在表達式中轉換。

自定義轉換

有時候轉換應該是自動的在特定類型之間。例如,設置背景的時候:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

這里,背景需要Drawable對象,但顏色是一個整數。不管何時有Drawable并且返回值是一個整數,那么整數類型會被轉換為ColorDrawable。這個轉換是通過使用帶有BindingConversion注解的靜態方法完成的:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

注意:轉換僅僅發生在setter級別,因此它不允許以下混合類型:

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Android Studio 對數據綁定的支持

Android Studio 支持數據綁定表達式的語法高亮,并可在編輯器中標示表達式語法錯誤。

預覽窗格可顯示數據綁定表達式的預設默認值,在下面的例子中,預覽窗格在 TextView 中顯示默認的 PLACEHOLDER 文本值。

<TextView android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{user.firstName, default=PLACEHOLDER}"/>

如果你需要在設計期間顯示默認值,你也可以使用 tools:attributes 代替默認表達式值。

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容