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

寫在前面

要學(xué)習(xí)新東西,最好的辦法是先學(xué)會(huì)如何使用。所以,本文僅作 Android Data Binding 的介紹并結(jié)合 DataBindingDemo 來(lái)理解它的用法,后續(xù)再對(duì)其原理進(jìn)行深入探討。

簡(jiǎn)介

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

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

Data Binding 是一個(gè)support庫(kù),最低支持到Android 2.1(API Level 7+)。

Data Binding 之前,我們不可避免地要編寫大量的毫無(wú)營(yíng)養(yǎng)的代碼,如 findViewById()、setText(),setVisibility(),setEnabled() 或 setOnClickListener() 等,通過(guò) Data Binding , 我們可以通過(guò)聲明式布局以精簡(jiǎn)的代碼來(lái)綁定應(yīng)用程序邏輯和布局,這樣就不用編寫大量的毫無(wú)營(yíng)養(yǎng)的代碼了。

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

  1. 首先,確保能使用Data Binding,需要下載最新的 Support repository。否則可能報(bào)錯(cuò),如圖:

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

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

    注意:如果app依賴了一個(gè)使用 Data Binding 的庫(kù),那么app module 的 build.gradle 也必須配置 Data Binding。

Data Binding 布局文件 - (View)

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

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
        <!-- 變量user, 描述了一個(gè)布局中會(huì)用到的屬性 -->
       <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á)式使用 “@{}” 的語(yǔ)法 -->
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

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

假設(shè)你有一個(gè) plain-old Java object(POJO) 的 User 對(duì)象。

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 對(duì)象:

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 的角度看,這兩個(gè)類是一樣的。用于 TextView 的 android:text 屬性的表達(dá)式@{user.firstName},會(huì)讀取 POJO 對(duì)象的 firstName 字段以及 JavaBeans 對(duì)象的 getFirstName()方法。

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

在默認(rèn)情況下,會(huì)基于布局文件生成一個(gè)繼承于 ViewDataBinding 的 Binding 類,將它轉(zhuǎn)換成帕斯卡命名并在名字后面接上Binding。例如,布局文件叫 main_activity.xml,所以會(huì)生成一個(gè) MainActivityBinding 類。這個(gè)類包含了布局文件中所有的綁定關(guān)系,會(huì)根據(jù)綁定表達(dá)式給布局文件賦值。在 inflate 的時(shí)候創(chuàng)建 binding 的方法如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   
   //  ActivityBaseBinding 類是自動(dòng)生成的
   ActivityBaseBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_base);
   User user = new User("Connor", "Lin");
   // 所有的 set 方法也是根據(jù)布局中 variable 名稱生成的
   binding.setUser(user);
}

事件處理

本部分源碼請(qǐng)參考 DataBindingDemo -> EventActivity 部分。

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

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

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

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

方法調(diào)用

相較于 android:onClick ,它的優(yōu)勢(shì)在于表達(dá)式會(huì)在編譯時(shí)處理,如果函數(shù)不存在或者函數(shù)簽名不對(duì),編譯將會(huì)報(bào)錯(cuò)。

以下是個(gè)例子:

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)聽器對(duì)象必須對(duì)應(yīng) -->
        <!-- 函數(shù)調(diào)用也可以使用 `.` , 如handler.onClickFriend , 不過(guò)已棄用 -->
    </LinearLayout>
</layout>

監(jiān)聽綁定

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

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

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

以下是個(gè)例子:

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)一個(gè)回調(diào)函數(shù)在表達(dá)式中使用時(shí),數(shù)據(jù)綁定會(huì)自動(dòng)為事件創(chuàng)建必要的監(jiān)聽器并注冊(cè)監(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á)式可添加一個(gè)或多個(gè)參數(shù),同時(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á)式會(huì)使布局難以閱讀和維護(hù),這種情況我們最好將業(yè)務(wù)邏輯寫到回調(diào)函數(shù)中

  • 也有一些特殊的點(diǎn)擊事件 我們需要使用不同于 android:onClick 的屬性來(lái)避免沖突。

下面是一些用來(lái)避免沖突的屬性:

Class Listener Setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

布局詳情

本部分源碼請(qǐng)參考 DataBindingDemo -> CombineActivity 部分

導(dǎo)入(Imports)

  • data 標(biāo)簽內(nèi)可以有多個(gè) 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ā)生沖突時(shí),可以使用 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 還沒(méi)有對(duì)導(dǎo)入提供自動(dòng)補(bǔ)全的支持。你的應(yīng)用還是可以被正常編譯,要解決這個(gè)問(wèn)題,你可以在變量定義中使用完整的包名。

  • 導(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.* 包中的類會(huì)被自動(dòng)導(dǎo)入,可以直接使用,例如, 要定義一個(gè) String 類型的變量
<variable name="test" type="String" />

變量 Variables

  • data 標(biāo)簽中可以有任意數(shù)量的 variable 標(biāo)簽。每個(gè) variable 標(biāo)簽描述了會(huì)在 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,引用時(shí)采用駝峰命名法。
<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 類會(huì)生成一個(gè)命名為 context 的特殊變量(其實(shí)就是 rootView 的 getContext() ) 的返回值),這個(gè)變量可用于表達(dá)式中。 如果有名為 context 的變量存在,那么生成的這個(gè) 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é)尾。這個(gè)類會(huì)被放置在 databinding 包中。舉個(gè)例子,布局文件 contact_item.xml 會(huì)生成 ContactItemBinding 類。如果 module 包名為 com.example.my.app ,binding 類會(huì)被放在 com.example.my.app.databinding 中。

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

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

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

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

這樣的話, CustomBinding 會(huì)直接生成在 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é)點(diǎn)。舉個(gè)例子, 以下的代碼<font color = "red">不能正常運(yùn)行 </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á)式語(yǔ)言

通用特性

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

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

例子:

<!-- 內(nèi)部使用字符串 & 字符拼接-->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{`Age :` + String.valueOf(user.age)}"/>

<!-- 三目運(yùn)算-->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

在xml中轉(zhuǎn)義是不可避免的,如 : 使用“&&”是編譯不通過(guò)的,需要使用轉(zhuǎn)義字符 "&&"

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

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

不支持的操作符

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

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

Null合并運(yùn)算符

Null合并運(yùn)算符 ?? 會(huì)在非 null 的時(shí)候選擇左邊的操作,反之選擇右邊。

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

等同于

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

容器類

通用的容器類:數(shù)組,lists,sparse lists,和 maps,可以用 [] 操作符來(lái)存取

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

字符串常量

使用單引號(hào)把屬性包起來(lái),就可以很簡(jiǎn)單地在表達(dá)式中使用雙引號(hào):

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

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

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

資源

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

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

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

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

當(dāng)復(fù)數(shù)形式有多個(gè)參數(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ù)對(duì)象 (Data Objects)

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

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

  • Observable 對(duì)象
  • Observable 字段
  • Observable 容器類

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

Observable 對(duì)象

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

Observable 接口有一個(gè)添加/移除 listener 的機(jī)制,但通知取決于開發(fā)者。為了簡(jiǎn)化開發(fā),Android 原生提供了一個(gè)基類 BaseObservable 來(lái)實(shí)現(xiàn) listener 注冊(cè)機(jī)制。這個(gè)類也實(shí)現(xiàn)了字段變動(dòng)的通知,只需要在 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 是編譯階段生成的一個(gè)類,功能與 R.java 類似,用 @Bindable 標(biāo)記過(guò) getter 方法會(huì)在 BR 中生成一個(gè) entry。
當(dāng)數(shù)據(jù)發(fā)生變化時(shí)需要調(diào)用 notifyPropertyChanged(BR.firstName) 通知系統(tǒng) BR.firstName 這個(gè) entry 的數(shù)據(jù)已經(jīng)發(fā)生變化以更新UI。

ObservableFields

創(chuàng)建 Observable 類還是需要花費(fèi)一點(diǎn)時(shí)間的,如果想要省時(shí),或者數(shù)據(jù)類的字段很少的話,可以使用 ObservableField 以及它的派生 ObservableBoolean、
ObservableByte 、ObservableCharObservableShort、ObservableIntObservableLong、ObservableFloat、ObservableDouble
ObservableParcelable

ObservableFields 是包含 observable 對(duì)象的單一字段。原始版本避免了在存取過(guò)程中做打包/解包操作。要使用它,在數(shù)據(jù)類中創(chuàng)建一個(gè) 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)用會(huì)使用更加靈活的結(jié)構(gòu)來(lái)保持?jǐn)?shù)據(jù)。Observable 容器類允許使用 key 來(lái)獲取這類數(shù)據(jù)。當(dāng) key 是類似 String 的一類引用類型時(shí),使用 ObservableArrayMap 會(huì)非常方便。

ObservableArrayMap<String, String> mUser = new ObservableArrayMap<>();
mUser.put("firstName", "Connor");
mUser.put("lastName", "Lin");
mUser.put("age", "28");
mBinding.setUser(mUser);

在布局中,可以用 String key 來(lái)獲取 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ù)類型時(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 與變量綁定在一起。就像先前提到過(guò)的,類名和包名可以自定義 。生成的 binding 類會(huì)繼承 ViewDataBinding 。

Creating

binding 應(yīng)該在 inflate 之后創(chuàng)建,確保 View 的層次結(jié)構(gòu)不會(huì)在綁定前被干擾。綁定布局有好幾種方式。最常見的是使用 binding 類中的靜態(tài)方法。inflate 函數(shù)會(huì) inflate View 并將 View 綁定到 binding 類上。此外有更加簡(jiǎn)單的函數(shù),只需要一個(gè) LayoutInflater 或一個(gè) ViewGroup:

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

如果布局使用不同的機(jī)制來(lái) inflate,則可以獨(dú)立做綁定操作:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

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

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

Views With IDs

布局中每一個(gè)帶有 ID 的 View,都會(huì)生成一個(gè) public final 字段。binding 過(guò)程會(huì)做一個(gè)簡(jiǎn)單的賦值,在 binding 類中保存對(duì)應(yīng) ID 的 View。這種機(jī)制相比調(diào)用 findViewById 效率更高。舉個(gè)例子:

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

將會(huì)在 binding 類內(nèi)生成:

public final TextView firstName;
public final TextView lastName;

ID 在 Data Binding 中并不是必需的,但是在某些情況下還是有必要對(duì) View 進(jìn)行操作。

Variables

每一個(gè)變量會(huì)有相應(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 類中生成對(duì)應(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

本部分源碼請(qǐng)參考 DataBindingDemo -> ViewStubActivity 部分。

ViewStub 相比普通 View 有一些不同。ViewStub 一開始是不可見的,當(dāng)它們被設(shè)置為可見,或者調(diào)用 inflate 方法時(shí),ViewStub 會(huì)被替換成另外一個(gè)布局。

因?yàn)?ViewStub 實(shí)際上不存在于 View 結(jié)構(gòu)中,binding 類中的類也得移除掉,以便系統(tǒng)回收。因?yàn)?binding 類中的 View 都是 final 的,所以Android 提供了一個(gè)叫 ViewStubProxy 的類來(lái)代替 ViewStub 。開發(fā)者可以使用它來(lái)操作 ViewStub,獲取 ViewStub inflate 時(shí)得到的視圖。

但 inflate 一個(gè)新的布局時(shí),必須為新的布局創(chuàng)建一個(gè) binding。因此, ViewStubProxy 必須監(jiān)聽 ViewStub 的 ViewStub.OnInflateListener,并及時(shí)建立 binding。由于 ViewStub 只能有一個(gè) OnInflateListener,你可以將你自己的 listener 設(shè)置在 ViewStubProxy 上,在 binding 建立之后, listener 就會(huì)被觸發(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 實(shí)例,為 ViewStubProy 注冊(cè) 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);
    }
});

通過(guò) ViewStubProxy 來(lái) inflate ViewStub :

public void inflate(View view) {
    if (!mActivityViewStubBinding.viewStub.isInflated()) {
        mActivityViewStubBinding.viewStub.getViewStub().inflate();
    }
}

此處 isInflated()getViewStub() 會(huì)標(biāo)紅,請(qǐng)不要擔(dān)心,這并不是錯(cuò)誤,是 ViewStubProxy 中的方法。

高級(jí)綁定

動(dòng)態(tài)變量

有時(shí)候,有一些不可知的 binding 類。例如,RecyclerView.Adapter 可以用來(lái)處理不同布局,這樣的話它就不知道應(yīng)該使用哪一個(gè) binding 類。而在 onBindViewHolder(VH, int) ) 的時(shí)候,binding 類必須被賦值。

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

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

以上,詳細(xì)請(qǐng)參考 DataBindingDemo -> MainActivity 部分(使用 RecyclerView 實(shí)現(xiàn))。

直接 binding

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

后臺(tái)線程

只要數(shù)據(jù)不是容器類,你可以直接在后臺(tái)線程做數(shù)據(jù)變動(dòng)。Data binding 會(huì)將變量/字段轉(zhuǎn)為局部量,避免同步問(wèn)題。


屬性設(shè)置

本部分源碼請(qǐng)參考 DataBindingDemo -> AttributeSettersActivity 部分。

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

自動(dòng)設(shè)置屬性

對(duì)一個(gè) attribute 來(lái)說(shuō),Data Binding 會(huì)嘗試尋找對(duì)應(yīng)的 setAttribute 函數(shù)。屬性的命名空間不會(huì)對(duì)這個(gè)過(guò)程產(chǎn)生影響,只有屬性的命名才是決定因素。

舉個(gè)例子,針對(duì)一個(gè)與 TextView 的 android:text 綁定的表達(dá)式,Data Binding會(huì)自動(dòng)尋找 setText(String) 函數(shù)。如果表達(dá)式返回值為 int 類型, Data Binding則會(huì)尋找 setText(int) 函數(shù)。所以需要小心處理函數(shù)的返回值類型,必要的時(shí)候使用強(qiáng)制類型轉(zhuǎn)換。

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

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

重命名屬性設(shè)置

一些屬性的命名與 setter 不對(duì)應(yīng)。針對(duì)這些函數(shù),可以用 BindingMethods 注解來(lái)將屬性與 setter 綁定在一起。舉個(gè)例子, android:tint 屬性可以這樣與 setImageTintList(ColorStateList) ) 綁定,而不是 setTint :

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

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

自定義屬性設(shè)置

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

Android 屬性已經(jīng)內(nèi)置一些 BindingAdapter。例如,這是一個(gè) paddingLeft 的自定義 setter:

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

Binding adapter 在其他自定義類型上也很好用。舉個(gè)例子,一個(gè) loader 可以在非主線程加載圖片。

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

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

// 無(wú)需手動(dòng)調(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存在時(shí),會(huì)自動(dòng)調(diào)用注解方法,即loadImage()-->
<ImageView 
    app:imageUrl=“@{url}”
    app:error=“@{@drawable/ic_launcher}”/>

當(dāng) imageUrl 與 error 存在時(shí)這個(gè) adapter 會(huì)被調(diào)用。imageUrl 是一個(gè) String,error 是一個(gè) Drawable。

  • 在匹配時(shí)自定義命名空間會(huì)被忽略
  • 你可以為 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 僅可用于只擁有一個(gè)抽象方法的接口或者抽象類。例如:

@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)置多個(gè)函數(shù)時(shí),必須分割成多個(gè) listener。例如, View.OnAttachStateChangeListener 內(nèi)置兩個(gè)函數(shù): onViewAttachedToWindow()onViewDetachedFromWindow() 。在這里必須為兩個(gè)不同的屬性創(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);
}

因?yàn)楦淖円粋€(gè) listener 會(huì)影響到另外一個(gè),我們必須編寫三個(gè)不同的 adapter,包括修改一個(gè)屬性的和修改兩個(gè)屬性的。

@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ù)雜,因?yàn)?View 是 add/remove View.OnAttachStateChangeListener 而不是 setandroid.databinding.adapters.ListenerUtil可以用來(lái)輔助跟蹤舊的 listener 并移除它。

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


轉(zhuǎn)換器Converters

對(duì)象轉(zhuǎn)換

當(dāng) binding 表達(dá)式返回對(duì)象時(shí),會(huì)選擇一個(gè) setter(自動(dòng) Setter,重命名 Setter,自定義 Setter),將返回對(duì)象強(qiáng)制轉(zhuǎn)換成 setter 需要的類型。

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

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

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

自定義轉(zhuǎn)換

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

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

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

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

需要注意的是,這個(gè)轉(zhuǎn)換只能在 setter 階段生效,所以 不允許 混合類型:

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

Android Studio對(duì)Data Binding的支持

  • Android Studio 支持 Data Binding 表現(xiàn)為:

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

注意:數(shù)組和泛型類型,如 Observable 類,當(dāng)沒(méi)有錯(cuò)誤時(shí)可能會(huì)顯示錯(cuò)誤。

  • 在預(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è)計(jì)階段顯示默認(rèn)值,你可以使用 tools 屬性代替默認(rèn)值表達(dá)式,詳見 設(shè)計(jì)階段布局屬性

參考資料

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

我的簡(jiǎn)書賬號(hào)是 ConnorLin,歡迎關(guān)注!

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容