Android Data Binding 系列(一) -- 詳細(xì)介紹與使用

寫在前面

要學(xué)習(xí)新東西莲组,最好的辦法是先學(xué)會如何使用绿聘。所以惰爬,本文僅作 Android Data Binding 的介紹并結(jié)合 DataBindingDemo 來理解它的用法金刁,后續(xù)再對其原理進行深入探討循签。

簡介

Data binding 在2015年7月發(fā)布的Android Studio v1.3.0 版本上引入级乐,在2016年4月Android Studio v2.0.0 上正式支持。目前為止县匠,Data Binding 已經(jīng)支持雙向綁定了风科。

Databinding 是一個實現(xiàn)數(shù)據(jù)和UI綁定的框架,是一個實現(xiàn) MVVM 模式的工具乞旦,有了 Data Binding贼穆,在Android中也可以很方便的實現(xiàn)MVVM開發(fā)模式。

Data Binding 是一個support庫兰粉,最低支持到Android 2.1(API Level 7+)故痊。

Data Binding 之前,我們不可避免地要編寫大量的毫無營養(yǎng)的代碼玖姑,如 findViewById()愕秫、setText(),setVisibility()焰络,setEnabled() 或 setOnClickListener() 等戴甩,通過 Data Binding , 我們可以通過聲明式布局以精簡的代碼來綁定應(yīng)用程序邏輯和布局,這樣就不用編寫大量的毫無營養(yǎng)的代碼了闪彼。

構(gòu)建環(huán)境

  1. 首先甜孤,確保能使用Data Binding,需要下載最新的 Support repository畏腕。否則可能報錯课蔬,如圖:

  2. 在模塊的build.gradle文件中添加dataBinding配置

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

    注意:如果app依賴了一個使用 Data Binding 的庫,那么app module 的 build.gradle 也必須配置 Data Binding郊尝。

Data Binding 布局文件 - (View)

Data binding 的布局文件與傳統(tǒng)布局文件有一點不同二跋。它以一個 layout 標(biāo)簽作為根節(jié)點,里面是 data 標(biāo)簽與 view 標(biāo)簽流昏。view 標(biāo)簽的內(nèi)容就是不使用 Data Binding 時的普通布局文件內(nèi)容扎即。以下是一個例子:

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

        <!-- 布局文件中的表達(dá)式使用 “@{}” 的語法 -->
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

數(shù)據(jù)對象 - (Model)

假設(shè)你有一個 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 屬性的表達(dá)式@{user.firstName},會讀取 POJO 對象的 firstName 字段以及 JavaBeans 對象的 getFirstName()方法闷营。

綁定數(shù)據(jù) - (ViewModel)

在默認(rèn)情況下烤黍,會基于布局文件生成一個繼承于 ViewDataBinding 的 Binding 類,將它轉(zhuǎn)換成帕斯卡命名并在名字后面接上Binding傻盟。例如速蕊,布局文件叫 main_activity.xml,所以會生成一個 MainActivityBinding 類娘赴。這個類包含了布局文件中所有的綁定關(guān)系规哲,會根據(jù)綁定表達(dá)式給布局文件賦值。在 inflate 的時候創(chuàng)建 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 方法也是根據(jù)布局中 variable 名稱生成的
   binding.setUser(user);
}

事件處理

本部分源碼請參考 DataBindingDemo -> EventActivity 部分诽表。

類似于 android:onClick 可以指定 Activity 中的函數(shù)唉锌,Data Binding 也允許處理從視圖中發(fā)送的事件。

有兩種實現(xiàn)方式:

  • 方法調(diào)用
  • 監(jiān)聽綁定

二者主要區(qū)別在于方法調(diào)用在編譯時處理竿奏,而監(jiān)聽綁定于事件發(fā)生時處理袄简。

方法調(diào)用

相較于 android:onClick ,它的優(yōu)勢在于表達(dá)式會在編譯時處理泛啸,如果函數(shù)不存在或者函數(shù)簽名不對绿语,編譯將會報錯。

以下是個例子:

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

表達(dá)式如下:

<?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}"/>
        <!-- 注意:函數(shù)名和監(jiān)聽器對象必須對應(yīng) -->
        <!-- 函數(shù)調(diào)用也可以使用 `.` , 如handler.onClickFriend , 不過已棄用 -->
    </LinearLayout>
</layout>

監(jiān)聽綁定

監(jiān)聽綁定在事件發(fā)生時調(diào)用平痰,可以使用任意表達(dá)式

此功能在 Android Gradle Plugin version 2.0 或更新版本上可用.

在方法引用中汞舱,方法的參數(shù)必須與監(jiān)聽器對象的參數(shù)相匹配。在監(jiān)聽綁定中宗雇,只要返回值與監(jiān)聽器對象的預(yù)期返回值相匹配即可昂芜。

以下是個例子:

public void onTaskClick(Task task) {
    task.run();
}

表達(dá)式如下:

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

當(dāng)一個回調(diào)函數(shù)在表達(dá)式中使用時,數(shù)據(jù)綁定會自動為事件創(chuàng)建必要的監(jiān)聽器并注冊監(jiān)聽赔蒲。

關(guān)于參數(shù)
  • 參數(shù)有兩種選擇:要么不寫泌神,要么就要寫全。
<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 表達(dá)式可添加一個或多個參數(shù)舞虱,同時參數(shù)可任意命名
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)}" />
  • 表達(dá)式結(jié)果有默認(rèn)值 null欢际、0、false等等

  • 表達(dá)式中可以使用void

<Button 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}" />
關(guān)于表達(dá)式
  • 復(fù)雜的表達(dá)式會使布局難以閱讀和維護矾兜,這種情況我們最好將業(yè)務(wù)邏輯寫到回調(diào)函數(shù)中

  • 也有一些特殊的點擊事件 我們需要使用不同于 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 部分

導(dǎo)入(Imports)

  • data 標(biāo)簽內(nèi)可以有多個 import 標(biāo)簽。你可以在布局文件中像使用 Java 一樣導(dǎo)入引用
<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}"/>
  • 當(dāng)類名發(fā)生沖突時椅寺,可以使用 alias
<import type="android.view.View"/>
<import type="com.connorlin.databinding.ui.View" alias="AliasView"/>
  • 導(dǎo)入的類型也可以用于變量的類型引用和表達(dá)式中
<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 還沒有對導(dǎo)入提供自動補全的支持浑槽。你的應(yīng)用還是可以被正常編譯蒋失,要解決這個問題,你可以在變量定義中使用完整的包名桐玻。

  • 導(dǎo)入也可以用于在表達(dá)式中使用靜態(tài)方法
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.* 包中的類會被自動導(dǎo)入篙挽,可以直接使用,例如镊靴, 要定義一個 String 類型的變量
<variable name="test" type="String" />

變量 Variables

  • data 標(biāo)簽中可以有任意數(shù)量的 variable 標(biāo)簽铣卡。每個 variable 標(biāo)簽描述了會在 binding 表達(dá)式中使用的屬性。
<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>
  • 可以在表達(dá)式中直接引用帶 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() ) 的返回值)苫耸,這個變量可用于表達(dá)式中州邢。 如果有名為 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);
}

自定義綁定類名

默認(rèn)情況下,binding 類的名稱取決于布局文件的命名骗村,以大寫字母開頭嫌褪,移除下劃線,后續(xù)字母大寫并追加 “Binding” 結(jié)尾胚股。這個類會被放置在 databinding 包中笼痛。舉個例子,布局文件 contact_item.xml 會生成 ContactItemBinding 類琅拌。如果 module 包名為 com.example.my.app 缨伊,binding 類會被放在 com.example.my.app.databinding 中。

通過修改 data 標(biāo)簽中的 class 屬性进宝,可以修改 Binding 類的命名與位置刻坊。舉個例子:

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

以上會在 databinding 包中生成名為 CustomBinding 的 binding 類。如果需要放置在不同的包下党晋,可以在前面加 “.”

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

這樣的話谭胚, CustomBinding 會直接生成在 module 包下。如果提供完整的包名未玻,binding 類可以放置在任何包名中:

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

Includes

在使用應(yīng)用命名空間的布局中灾而,變量可以傳遞到任何 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 節(jié)點。舉個例子庇绽, 以下的代碼<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>

表達(dá)式語言

通用特性

表達(dá)式語言與 Java 表達(dá)式有很多相似之處锡搜。下面是相同之處:

  • 數(shù)學(xué)計算 + - / * %
  • 字符串連接 +
  • 邏輯 && ||
  • 二進制 & | ^
  • 一元 + - ! ~
  • 位移 >> >>> <<
  • 比較 == > < >= <=
  • instanceof
  • 組 ()
  • 字面量 - 字符癣猾,字符串,數(shù)字余爆, null
  • 類型轉(zhuǎn)換
  • 函數(shù)調(diào)用
  • 字段存取
  • 數(shù)組存取 []
  • 三元運算符 ?:

例子:

<!-- 內(nèi)部使用字符串 & 字符拼接-->
<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中轉(zhuǎn)義是不可避免的纷宇,如 : 使用“&&”是編譯不通過的,需要使用轉(zhuǎn)義字符 "&&"

附:常用的轉(zhuǎn)義字符

顯示結(jié)果 描述 轉(zhuǎn)義字符 十進制
空格 ? ?
< 小于號 < <
> 大于號 > >
& 與號 & &
" 引號 " "
' 撇號 ' '
× 乘號 × ×
÷ 除號 ÷ ÷

不支持的操作符

一些 Java 中的操作符在表達(dá)式語法中不能使用蛾方。

  • this
  • super
  • new
  • 顯式泛型調(diào)用 <T>

Null合并運算符

Null合并運算符 ?? 會在非 null 的時候選擇左邊的操作像捶,反之選擇右邊。

android:text="@{user.lastName ?? `Default LastName`}"

等同于

android:text="@{user.lastName != null ? user.lastName : `Default LastName`}"

容器類

通用的容器類:數(shù)組桩砰,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]}"

字符串常量

使用單引號把屬性包起來硼莽,就可以很簡單地在表達(dá)式中使用雙引號:

android:text='@{map["firstName"]}'

也可以用雙引號將屬性包起來。這樣的話煮纵,字符串常量就可以用 " 或者反引號 ( ` ) 來調(diào)用

android:text="@{map[`firstName`}"
android:text="@{map["firstName"]}"

資源

也可以在表達(dá)式中使用普通的語法來引用資源:

android:text="@{@string/fullname(user.fullName)"

字符串格式化和復(fù)數(shù)形式可以這樣實現(xiàn):

android:text="@{@plurals/sample_plurals(num)}"

當(dāng)復(fù)數(shù)形式有多個參數(shù)時懂鸵,應(yīng)該這樣寫:

android:text="@{@plurals/numbers(num, num)}"

一些資源需要顯示類型調(diào)用。

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

數(shù)據(jù)對象 (Data Objects)

任何 POJO 對象都能用在 Data Binding 中行疏,但是更改 POJO 并不會同步更新 UI匆光。Data Binding 的強大之處就在于它可以讓你的數(shù)據(jù)擁有更新通知的能力。

有三種不同的動態(tài)更新數(shù)據(jù)的機制:

  • Observable 對象
  • Observable 字段
  • Observable 容器類

當(dāng)以上的 observable 對象綁定在 UI 上酿联,數(shù)據(jù)發(fā)生變化時终息,UI 就會同步更新。

Observable 對象

當(dāng)一個類實現(xiàn)了 Observable 接口時贞让,Data Binding 會設(shè)置一個 listener 在綁定的對象上周崭,以便監(jiān)聽對象字段的變動。

Observable 接口有一個添加/移除 listener 的機制喳张,但通知取決于開發(fā)者续镇。為了簡化開發(fā),Android 原生提供了一個基類 BaseObservable 來實現(xiàn) listener 注冊機制蹲姐。這個類也實現(xiàn)了字段變動的通知磨取,只需要在 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 標(biāo)記過 getter 方法會在 BR 中生成一個 entry江咳。
當(dāng)數(shù)據(jù)發(fā)生變化時需要調(diào)用 notifyPropertyChanged(BR.firstName) 通知系統(tǒng) BR.firstName 這個 entry 的數(shù)據(jù)已經(jīng)發(fā)生變化以更新UI逢净。

ObservableFields

創(chuàng)建 Observable 類還是需要花費一點時間的,如果想要省時,或者數(shù)據(jù)類的字段很少的話爹土,可以使用 ObservableField 以及它的派生 ObservableBoolean甥雕、
ObservableByteObservableChar胀茵、ObservableShort社露、ObservableIntObservableLong琼娘、ObservableFloat峭弟、ObservableDouble
ObservableParcelable 脱拼。

ObservableFields 是包含 observable 對象的單一字段瞒瘸。原始版本避免了在存取過程中做打包/解包操作。要使用它熄浓,在數(shù)據(jù)類中創(chuàng)建一個 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);
    }
}

要存取數(shù)據(jù)情臭,只需要使用 get() / set() 方法:

mObservableFieldContact.mName.set("ConnorLin");
mObservableFieldContact.mPhone.set("12345678901");

String name = mObservableFieldContact.mName.get();

Observable Collections 容器類

一些應(yīng)用會使用更加靈活的結(jié)構(gòu)來保持?jǐn)?shù)據(jù)。Observable 容器類允許使用 key 來獲取這類數(shù)據(jù)赌蔑。當(dāng) 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 中的數(shù)據(jù):

<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"]}'/>

當(dāng) key 是整數(shù)類型時朝巫,可以使用 ObservableArrayList :

ObservableArrayList<String> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add("17");

在布局文件中鸿摇,使用下標(biāo)獲取列表數(shù)據(jù):

<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 應(yīng)該在 inflate 之后創(chuàng)建,確保 View 的層次結(jié)構(gòu)不會在綁定前被干擾筷黔。綁定布局有好幾種方式往史。最常見的是使用 binding 類中的靜態(tài)方法。inflate 函數(shù)會 inflate View 并將 View 綁定到 binding 類上佛舱。此外有更加簡單的函數(shù)椎例,只需要一個 LayoutInflater 或一個 ViewGroup:

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

如果布局使用不同的機制來 inflate,則可以獨立做綁定操作:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有時綁定關(guān)系是不能提前確定的请祖。這種情況下订歪,可以使用 DataBindingUtil :

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

Views With IDs

布局中每一個帶有 ID 的 View,都會生成一個 public final 字段肆捕。binding 過程會做一個簡單的賦值刷晋,在 binding 類中保存對應(yīng) ID 的 View。這種機制相比調(diào)用 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 類內(nèi)生成:

public final TextView firstName;
public final TextView lastName;

ID 在 Data Binding 中并不是必需的眼虱,但是在某些情況下還是有必要對 View 進行操作喻奥。

Variables

每一個變量會有相應(yīng)的存取函數(shù):

<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 類中生成對應(yīng)的 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 一開始是不可見的撞蚕,當(dāng)它們被設(shè)置為可見,或者調(diào)用 inflate 方法時过牙,ViewStub 會被替換成另外一個布局诈豌。

因為 ViewStub 實際上不存在于 View 結(jié)構(gòu)中,binding 類中的類也得移除掉抒和,以便系統(tǒng)回收矫渔。因為 binding 類中的 View 都是 final 的,所以Android 提供了一個叫 ViewStubProxy 的類來代替 ViewStub 摧莽。開發(fā)者可以使用它來操作 ViewStub庙洼,獲取 ViewStub inflate 時得到的視圖。

但 inflate 一個新的布局時镊辕,必須為新的布局創(chuàng)建一個 binding油够。因此, ViewStubProxy 必須監(jiān)聽 ViewStub 的 ViewStub.OnInflateListener征懈,并及時建立 binding石咬。由于 ViewStub 只能有一個 OnInflateListener,你可以將你自己的 listener 設(shè)置在 ViewStubProxy 上卖哎,在 binding 建立之后鬼悠, listener 就會被觸發(fā)。

<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() 會標(biāo)紅焕窝,請不要擔(dān)心,這并不是錯誤维贺,是 ViewStubProxy 中的方法它掂。

高級綁定

動態(tài)變量

有時候,有一些不可知的 binding 類溯泣。例如虐秋,RecyclerView.Adapter 可以用來處理不同布局,這樣的話它就不知道應(yīng)該使用哪一個 binding 類垃沦。而在 onBindViewHolder(VH, int) ) 的時候客给,binding 類必須被賦值。

在這種情況下栏尚,RecyclerView 的布局內(nèi)置了一個 item 變量起愈。 BindingHolder 有一個 getBinding 方法只恨,返回一個 ViewDataBinding 基類。

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

以上抬虽,詳細(xì)請參考 DataBindingDemo -> MainActivity 部分(使用 RecyclerView 實現(xiàn))官觅。

直接 binding

當(dāng)變量或者 observable 發(fā)生變動時,會在下一幀觸發(fā) binding阐污。有時候 binding 需要馬上執(zhí)行休涤,這時候可以使用 executePendingBindings()

后臺線程

只要數(shù)據(jù)不是容器類笛辟,你可以直接在后臺線程做數(shù)據(jù)變動功氨。Data binding 會將變量/字段轉(zhuǎn)為局部量,避免同步問題手幢。


屬性設(shè)置

本部分源碼請參考 DataBindingDemo -> AttributeSettersActivity 部分捷凄。

當(dāng)綁定數(shù)據(jù)發(fā)生變動時,生成的 binding 類必須根據(jù) binding 表達(dá)式調(diào)用 View 的 setter 函數(shù)围来。Data binding 框架內(nèi)置了幾種自定義賦值的方法跺涤。

自動設(shè)置屬性

對一個 attribute 來說,Data Binding 會嘗試尋找對應(yīng)的 setAttribute 函數(shù)监透。屬性的命名空間不會對這個過程產(chǎn)生影響桶错,只有屬性的命名才是決定因素。

舉個例子胀蛮,針對一個與 TextView 的 android:text 綁定的表達(dá)式院刁,Data Binding會自動尋找 setText(String) 函數(shù)。如果表達(dá)式返回值為 int 類型粪狼, Data Binding則會尋找 setText(int) 函數(shù)退腥。所以需要小心處理函數(shù)的返回值類型,必要的時候使用強制類型轉(zhuǎn)換鸳玩。

需要注意的是阅虫,Data Binding 在對應(yīng)名稱的屬性不存在的時候也能繼續(xù)工作。你可以輕而易舉地使用 Data Binding 為任何 setter “創(chuàng)建” 屬性不跟。
DataBindingDemo 中的自定義布局 Card,并沒有添加 declare-styleable米碰,但是可以使用自動 setter 的特性來調(diào)用這些函數(shù)窝革。

<com.connorlin.databinding.view.Card
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:object="@{user}"/>

重命名屬性設(shè)置

一些屬性的命名與 setter 不對應(yīng)。針對這些函數(shù)吕座,可以用 BindingMethods 注解來將屬性與 setter 綁定在一起虐译。舉個例子, android:tint 屬性可以這樣與 setImageTintList(ColorStateList) ) 綁定吴趴,而不是 setTint :

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

Android 框架中的 setter 重命名已經(jīng)在庫中實現(xiàn)了漆诽,我們只需要專注于自己的 setter。

自定義屬性設(shè)置

一些屬性需要自定義 setter 邏輯。例如厢拭,目前沒有與 android:paddingLeft 對應(yīng)的 setter兰英,只有一個 setPadding(left, top, right, bottom) 函數(shù)吼鱼。結(jié)合靜態(tài) binding adapter 函數(shù)與 BindingAdapter 注解可以讓開發(fā)者自定義屬性 setter厌蔽。

Android 屬性已經(jīng)內(nèi)置一些 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 可以在非主線程加載圖片。

當(dāng)存在沖突時寨闹,開發(fā)者創(chuàng)建的 binding adapter 會覆蓋 Data Binding 的默認(rèn) adapter胶坠。

你也可以創(chuàng)建多個參數(shù)的 adapter:

// 無需手動調(diào)用此函數(shù)
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
    Glide.with(view.getContext()).load(url).error(error).into(view);
}
<!-- 當(dāng)url存在時,會自動調(diào)用注解方法繁堡,即loadImage()-->
<ImageView 
    app:imageUrl=“@{url}”
    app:error=“@{@drawable/ic_launcher}”/>

當(dāng) imageUrl 與 error 存在時這個 adapter 會被調(diào)用涵但。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);
        }
    }
}

當(dāng) listener 內(nèi)置多個函數(shù)時澈侠,必須分割成多個 listener。例如埋酬, View.OnAttachStateChangeListener 內(nèi)置兩個函數(shù): onViewAttachedToWindow()onViewDetachedFromWindow() 哨啃。在這里必須為兩個不同的屬性創(chuàng)建不同的接口。

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

上面的例子比普通情況下復(fù)雜珍特,因為 View 是 add/remove View.OnAttachStateChangeListener 而不是 set祝峻。 android.databinding.adapters.ListenerUtil可以用來輔助跟蹤舊的 listener 并移除它。

對應(yīng) addOnAttachStateChangeListener(View.OnAttachStateChangeListener) )支持的 api 版本扎筒,
通過向 OnViewDetachedFromWindowOnViewAttachedToWindow 添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1) 注解莱找,
Data Binding 代碼生成器會知道這些 listener 只會在 Honeycomb MR1 或更新的設(shè)備上使用。


轉(zhuǎn)換器Converters

對象轉(zhuǎn)換

當(dāng) binding 表達(dá)式返回對象時嗜桌,會選擇一個 setter(自動 Setter奥溺,重命名 Setter,自定義 Setter)骨宠,將返回對象強制轉(zhuǎn)換成 setter 需要的類型浮定。

下面是一個使用 ObservableMap 保存數(shù)據(jù)的例子:

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

在這里相满, userMap 會返回 Object 類型的值,而返回值會被自動轉(zhuǎn)換成 setText(CharSequence) 需要的類型桦卒。當(dāng)對參數(shù)類型存在疑惑時立美,開發(fā)者需要手動做類型轉(zhuǎn)換。

自定義轉(zhuǎn)換

有時候會自動在特定類型直接做類型轉(zhuǎn)換闸盔。例如悯辙,當(dāng)設(shè)置背景的時候:

<View
  android:background="@{isError.get() ? @color/colorAccent : @color/colorPrimary}"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>

在這里,背景需要的是 Drawable 迎吵,但是 color 是一個整數(shù)躲撰。當(dāng)需要 Drawable 卻返回了一個整數(shù)時, int 會自動轉(zhuǎn)換成 ColorDrawable 击费。這個轉(zhuǎn)換是在一個 BindingConversation 注解的靜態(tài)函數(shù)中實現(xiàn):

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

需要注意的是拢蛋,這個轉(zhuǎn)換只能在 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 表現(xiàn)為:

    • 語法高亮
    • 標(biāo)記表達(dá)式語法錯誤
    • XML 代碼補全
    • 跳轉(zhuǎn)到聲明或快速文檔

注意:數(shù)組和泛型類型蔫巩,如 Observable 類谆棱,當(dāng)沒有錯誤時可能會顯示錯誤。

  • 在預(yù)覽窗口可顯示 Data Binding 表達(dá)式的默認(rèn)值圆仔。例如:
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName, default=FirstName}"/>
  <!-- TextView 的 text 默認(rèn)值為 FirstName -->

如果你需要在設(shè)計階段顯示默認(rèn)值垃瞧,你可以使用 tools 屬性代替默認(rèn)值表達(dá)式,詳見 設(shè)計階段布局屬性

參考資料

  1. Data Binding Library
  2. 安卓 Data Binding 使用方法總結(jié)
  3. (譯)Data Binding 指南
  4. 精通 Android Data Binding

我的簡書賬號是 ConnorLin坪郭,歡迎關(guān)注个从!

我的簡書專題是 Android開發(fā)技術(shù)分享,歡迎關(guān)注歪沃!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嗦锐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子沪曙,更是在濱河造成了極大的恐慌奕污,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件液走,死亡現(xiàn)場離奇詭異碳默,居然都是意外死亡,警方通過查閱死者的電腦和手機育灸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門腻窒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人磅崭,你說我怎么就攤上這事⊥甙ィ” “怎么了砸喻?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵柔逼,是天一觀的道長。 經(jīng)常有香客問我割岛,道長愉适,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任癣漆,我火速辦了婚禮维咸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惠爽。我一直安慰自己癌蓖,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布婚肆。 她就那樣靜靜地躺著租副,像睡著了一般。 火紅的嫁衣襯著肌膚如雪较性。 梳的紋絲不亂的頭發(fā)上用僧,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音赞咙,去河邊找鬼责循。 笑死,一個胖子當(dāng)著我的面吹牛攀操,可吹牛的內(nèi)容都是我干的院仿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼崔赌,長吁一口氣:“原來是場噩夢啊……” “哼意蛀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起健芭,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤县钥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后慈迈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體若贮,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年痒留,在試婚紗的時候發(fā)現(xiàn)自己被綠了谴麦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡伸头,死狀恐怖匾效,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情恤磷,我是刑警寧澤面哼,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布野宜,位于F島的核電站,受9級特大地震影響魔策,放射性物質(zhì)發(fā)生泄漏匈子。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一闯袒、第九天 我趴在偏房一處隱蔽的房頂上張望虎敦。 院中可真熱鬧,春花似錦政敢、人聲如沸其徙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽擂橘。三九已至,卻和暖如春摩骨,著一層夾襖步出監(jiān)牢的瞬間通贞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工恼五, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留昌罩,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓灾馒,卻偏偏與公主長得像茎用,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子睬罗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容