深入原始碼學習 android data binding 之:data binding 註解

LemonYang發表於2016-12-01

雖然沒有開通專欄之前已經在挖金投稿過了這篇文章,但是我打算寫一個關於android data binding庫的一系列的文章,為了完整性,我還是在這裡重新發布一遍。如果之前已經看過這篇android data binding 實踐之:data binding 註解,那麼可以忽略下面的內容,如果你喜歡的話可以收藏也可以點贊哦!


其實在android data binding這個庫裡面核心的內容我覺得就是下面幾個:

  • 定義的一系列方便使用的註解
  • 註解處理器的邏輯(這部分程式碼量非常龐大其實)
  • 解決監聽和回撥的機制

上面這幾個核心內容我都會在接下來的文章中討論,幾天先把幾個關鍵的註解的使用進行簡單的分析。喜歡的可以點贊可以收藏哦!

Bindable

使用場景

data binding的意義主要資料的變動可以自動觸發UI介面的重新整理。但是如果我們使用的是傳統的java bean物件的時候,是沒有辦法實現“資料變更觸發ui介面”的目的的。而 Bindable 註解就是幫助我們完成這個任務的。

如果我們要實現“資料變更觸發ui介面”的話,途徑主要有兩個:

  1. 繼承 BaseObservable ,使用 Bindable 註解field的getter並且在呼叫setter的使用使用 OnPropertyChangedCallback#onPropertyChanged
  2. 使用data-binding library當中提供的諸如 ObservableField<> , ObservableInt作為屬性值

程式碼定義

/**
 * The Bindable annotation should be applied to any getter accessor method of an
 * {@link Observable} class. Bindable will generate a field in the BR class to identify
 * the field that has changed.
 *
 * @see OnPropertyChangedCallback#onPropertyChanged(Observable, int)
 */
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) // this is necessary for java analyzer to work
public @interface Bindable {

}複製程式碼

使用解析

根據上面程式碼中的註釋我們可以知道,Bindable 是對繼承了 Observable 的類當中的 getter 方法進行註解。並且當我們使用了這個註解的時候,databinding library 會在 BR 這個類當中生成一個屬性,用以標識發生了變化的屬性field

使用示例

public class PrivateUser extends BaseObservable{

    private String fistName;
    private String lastName;

    public PrivateUser(String fistName, String lastName) {
        this.age = age;
        this.fistName = fistName;
        this.lastName = lastName;
    }

    @Bindable
    public String getFistName() {
        return fistName;
    }

    public void setFistName(String fistName) {
        this.fistName = fistName;
        notifyPropertyChanged(BR.fistName);
    }

    @Bindable
    public String getLastName() {
        return lastName;
    }

    public void setLasetName(String lasetName) {
        this.lastName = lastName;
        notifyPropertyChanged(BR.lastName);

    }
  }複製程式碼

使用心得

  1. 根據 Bindable 的定義可以發現,Bindable 是支援對屬性進行註解的,所以當我們的屬性是public的(不需要通過getter進行訪問)的時候,是可以在屬性上面使用該註解的。但是改變屬性的值的時候是一定要呼叫 onPropertyChanged() 這個方法的,否則無法實現通知重新整理UI的功能;
  2. 上面頻繁出現的 BR 這個類是在編譯的時候生成的,使用的時候可能會發現ide沒辦法找到我們的屬性,比如BR.lastName ,只要rebuild一下就可以了。

BindingAdapter

使用場景

當我們使用data binding的時候,data-binding library會盡可能的找到給view對應的屬性(Attribute)進行賦值的方法,通常這個方法的名稱就是set${Attribute}。這時候屬性前面的名稱空間會被忽略,而只關注屬性的名稱。比如我們資料繫結了TextView的 android:text 屬性,那麼data-binding library會去尋找 setText(String) 這樣的一個方法。

BindingAdapter 註解的方法能夠控制給view賦值的操作過程,也就是說可以實現自定義setter的實現。對於那些不具備對應setter方法的屬性(比如我們要繫結一個 android:paddingLeft 的屬性的時候,我們卻只有 setPadding(left, top, right, bottom) 這樣的一個setter方法),那麼我們可以通過BindingAdapter 註解來實現一個自定義的setter;比如我們希望給ImageView設定了一個字串URL的值的時候,ImageView能夠根據這個URL進行自主的聯網下載圖片的操作。

此外我們還可以覆蓋原有的setter方法的邏輯,比如我們使用 BindingAdapter 的時候引數傳入的是 android:text ,那麼我們方法的實現邏輯就會複寫原來的setText(String)的方法邏輯了

程式碼定義

@Target(ElementType.METHOD)
public @interface BindingAdapter {

    /**
     * @return The attributes associated with this binding adapter.
     */
    String[] value();

    /**
     * Whether every attribute must be assigned a binding expression or if some
     * can be absent. When this is false, the BindingAdapter will be called
     * when at least one associated attribute has a binding expression. The attributes
     * for which there was no binding expression (even a normal XML value) will
     * cause the associated parameter receive the Java default value. Care must be
     * taken to ensure that a default value is not confused with a valid XML value.
     *
     * @return whether or not every attribute must be assigned a binding expression. The default
     *         value is true.
     */
    boolean requireAll() default true;
}複製程式碼

使用解析

  • 使用了 BindingAdapter 註解的方法能夠控制給view賦值的操作過程。註解當中的引數就是我們需要關聯繫結的屬性址。關於這一點,官方程式碼註釋裡給了一個相當簡單的例項:
@BindingAdapter("android:bufferType")
public static void setBufferType(TextView view, TextView.BufferType bufferType) {
    view.setText(view.getText(), bufferType);
}複製程式碼

在上面的例子中,只要我們的TextView中設定了 android:bufferType 這個屬性,那麼 setBufferType 這個方法就會被回撥

  • 在使用了 BindingAdapter 註解的方法中我們還可以拿到該屬性設定的之前的value值,關於這一點,官方的註釋中同樣給出了一個簡單的例項:
@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);
        }
    }
}複製程式碼
  • BindingAdapter 還可以設定多個屬性引數。當我們設定了多個屬性引數之後,只有當所有的屬性引數都被設定使用的時候才會回撥我們的方法。例如:
@BindingAdapter({"android:onClick", "android:clickable"})
public static void setOnClick(View view, View.OnClickListener clickListener, boolean clickable) {
    view.setOnClickListener(clickListener);
    view.setClickable(clickable);
}複製程式碼

當我們同時設定了多個屬性的時候,要注意引數的順序問題,方法當中的屬性值引數順序必須跟註解當中的屬性引數順序一致

  • 在上面的例子中,BindingAdapter 註解的都是類的方法,實際上例項方法我們也是可以使用這個註解的。當我們註解例項方法的時候,生成的 DataBindingComponent 會擁有一個getter方法獲得註解方法所在的類的物件的例項

使用示例

官方程式碼註釋中使用示例比較多,而且也很全面,這裡主要展示一下使用 BindingAdapter 註解的例項方法的使用。

首先是我們的XML佈局檔案:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data >

        <variable
            name="privateUser"
            type="net.uni_unity.databindingdemo.model.PrivateUser" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            app:text="@{privateUser}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="5dp" />
    </LinearLayout>
</layout>複製程式碼

接下來是我們要建立一個物件,裡面包含了我們的 BindingAdapter 註解的方法:

public class MyBindindAdapter {

    @BindingAdapter("app:text")
    public void setText(View textView, PrivateUser user) {
        Log.d("setText", "isCalled");
    }
}複製程式碼

然後是我們的activity:

public class MainActivity extends AppCompatActivity {

    private PrivateUser user;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //通過DataBindingUtil載入我們的佈局檔案
        //在這裡我們例項化了一個我們定義的DataBindingComponent物件MyComponent
        //這個是非常關鍵的一個地方,否則系統會使用DataBindingUtil#getDefaultComponent()拿到的預設例項作為引數
        //如果我們在此之前沒有呼叫DataBindingUtil.setDefaultComponent()方法,上面的方法就會返回null
        ActivityMainBinding activityBinding = DataBindingUtil.setContentView(this, R.layout.activity_main,new MyComponent());
        //將view與資料進行繫結
        user = new PrivateUser("privateFistName", "privateLastName", "private_user", 10);
        activityBinding.setPrivateUser(user);
    }複製程式碼

接下來就是我們實現了 DataBindingComponent 的物件

public class MyComponent implements android.databinding.DataBindingComponent {

    @Override
    public MyBindindAdapter getMyBindindAdapter() {
        return new MyBindindAdapter();
    }
}複製程式碼

我們會發現 getMyBindindAdapter() 這個方法是帶有 @Override 註解的,這是因為我們使用 BindingAdapter 註解一個例項方法的時候,data-binding library都會為我們在 DataBindingComponent 的實現中自動生成一個getter方法。

經過上面幾步我們就完成了使用 BindingAdapter 註解例項方法。其中的 關鍵的環節有兩個:

  1. 實現 DataBindingComponent 這個介面;
  2. 通過 DataBindingUtil.setDefaultComponent(); 方法設定我們的 DataBindingComponent 例項

使用心得

因為官方的程式碼註釋中給出了不少的例項,這裡主要說一下我們使用 BindingAdapter 註解的時候要注意的問題

  1. BindingAdapter 註解的方法可以定義在任何位置。不管我們的方法是例項方法還是類方法,也不管這些方法定義在哪個類裡面,只要我們加上了BindingAdapter 這個註解,只要引數匹配,被註解的方法就會被回撥;
  2. BindingAdapter 註解的方法可以選擇性的使用一個 DataBindingComponent 的實現類例項作為第一個引數,預設情況下DataBindingUtil裡面的相關inflate方法會使用 DataBindingUtil#getDefaultComponent()拿到的物件;
  3. BindingAdapter 是如何起作用的呢?其實 關鍵的地方主要是兩點

    • BindingAdapter 註解定義的引數,比如@BindingAdapter("android:onLayoutChange")

      BindingAdapter 當中的引數的名稱空間可以隨便定義,我們可以使用“android”,當然也可以使用“app”,只要引數的值跟佈局檔案中定義的值是一樣的就可以起作用;

    • 方法當中的定義的引數,比如setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,View.OnLayoutChangeListener newValue)

      方法當中第一個引數對應的就是我們繫結的view物件,可以是具體的我們繫結的view,比如TextView,也可以是對應的父類,比如說view。第二/三個引數對應的是繫結的屬性對應的value值,而且這個value值的型別要跟屬性裡面使用的value值是一致的,否則會不起作用,甚至是報錯。例如:

      我在佈局檔案中寫了下面的一段程式碼:

      <layout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
      
      <data >
        <variable
            name="privateUser"
            type="net.uni_unity.databindingdemo.model.PrivateUser" />
      </data>
      
      <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
      
        <TextView
            app:text="@{privateUser}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="5dp" />
        </LinearLayout>
      </layout>複製程式碼

      然後我在activity當中使用 BindingAdapter 註解了這樣的一個方法:

      @BindingAdapter("app:text")
      public static void setText(View textView, String user) {
        Log.d("setText", "isCalled");
      }複製程式碼

      這個時候我們編譯的時候就會收到這樣的error:

      java.lang.RuntimeException: Found data binding errors.
      / data binding error msg:Cannot find the setter for attribute 'app:text' with parameter type net.uni_unity.databindingdemo.model.PrivateUser on android.widget.TextView. file:/DataBindingDemo/app/src/main/res/layout/activity_main.xml loc:29:24 - 29:34 \ data binding error

      因為我們在方法中定義的引數型別是String ,可是我們在XML檔案中定義的型別是 PrivateUser ,因此就提示我們找不到引數匹配的setter方法了。

      如果我們把方法中引數隨便去掉一個,比如我們定義如下:

      @BindingAdapter("app:text")
      public static void setText(View textView) {
        Log.d("setText", "isCalled");
      }複製程式碼

      這時候我們就會得到另一個編譯錯誤:

      java.lang.RuntimeException: failure, see logs for details.@BindingAdapter setSimpleText(java.lang.String) has 1 attributes and 0 value parameters. There should be 1 or 2 value parameters.

      上面的兩個錯誤示例大概能夠讓我們明白 BindingAdapter 是怎樣工作的了。

BindingConversion

使用場景

當我們需要給view繫結的資料型別和view對應屬性的目標型別不一致的時候,我們通常的做法是先把資料型別轉換之後再與view進行繫結。但是我們使用了 BindingConversion 註解之後,就可以定義一個轉換器實現在呼叫setter方法設定屬性值的時候對資料進行轉換。

程式碼定義

/**
 * Annotate methods that are used to automatically convert from the expression type to the value
 * used in the setter. The converter should take one parameter, the expression type, and the
 * return value should be the target value type used in the setter. Converters are used
 * whenever they can be applied and are not specific to any attribute.
 */
@Target({ElementType.METHOD})
public @interface BindingConversion {
}複製程式碼

使用解析

根據官方程式碼中的註釋,我們可以理解到 BindingConversion 的用途:使用該註解的方法可以自動的將我們宣告的引數型別轉化成我們實際要使用的引數型別的值。註解的方法接受一個引數,這個引數的型別就是我們繫結的view物件宣告的型別,而方法的返回值則是我們繫結的屬性值的目標型別。要注意的一點是,這種轉換器可以被使用在任何時候,而並不需要對應特定的屬性。

使用示例

首先,我們在佈局檔案中定義如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data >
        <variable
            name="date"
            type="java.util.Date"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{date}"
            android:padding="5dp" />
    </LinearLayout>
</layout>複製程式碼

本來 TextViewandroid:text 屬性是接受一個 String 型別的值的,但是我們在XML當中繫結的卻是一個 Date 型別的值。如果這時候我們沒有在java檔案中定義Converter的話,那麼我們將會在編譯期得到這樣的一個error:

java.lang.RuntimeException: Found data binding errors.
/ data binding error msg:Cannot find the setter for attribute 'android:text' with parameter type java.util.Date on android.widget.TextView. file:/DataBindingDemo/app/src/main/res/layout/activity_main.xml loc:35:28 - 35:31 \ data binding error

但是在我們定義了下面的之後,一切就妥妥的了:

@BindingConversion
public static String convertDate(Date date){
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    return sdf.format(date);
}複製程式碼

使用心得

  1. BindingAdapter 不同,BindingConversion 是不支援例項方法的,如果我們試圖將上面的方法定義成例項方法,我們將會在編譯期得到這樣的一個error:

java.lang.RuntimeException: failure, see logs for details.
@BindingConversion is only allowed on public static methods convertDate(java.util.Date)

BindingMethod

使用場景

有的屬性我們的view屬性的setter方法跟屬性的名稱並不相匹配(因為data-bing是通過setAttr的形式去尋找對應的setter方法的)。比如說“android:tint”這個屬性對應的setter方法名稱是 setImageTintList(ColorStateList) ,而不是 setTint() 方法。這時候使用 BindingMethod 註解可以幫助我們重新命名view屬性對應的setter方法名稱。

程式碼定義

/**
 * Used within an {@link BindingMethods} annotation to describe a renaming of an attribute to
 * the setter used to set that attribute. By default, an attribute attr will be associated with
 * setter setAttr.
 */
@Target(ElementType.ANNOTATION_TYPE)
public @interface BindingMethod {

    /**
     * @return the View Class that the attribute is associated with.
     */
    Class type();

    /**
     * @return The attribute to rename. Use android: namespace for all android attributes or
     * no namespace for application attributes.
     */
    String attribute();

    /**
     * @return The method to call to set the attribute value.
     */
    String method();
}複製程式碼

使用解析

根據官方程式碼中的註釋我們可以發現,BindingMethod 是放在 BindingMethods 註解當中作為引數陣列的一個元素進行使用的。
使用的時候,我們需要在註解引數中指定三個關鍵值,分別是:

  • type:屬性所關聯的view的class物件
  • attribute: 屬性的名稱(如果是android框架的屬性使用“android”作為名稱空間,如果是應用本身定義的屬性則不需要附帶名稱空間)
  • method:對應的setter方法名稱

使用示例

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

BindingMethods

使用場景

BindingMethodsBindingMethod 結合使用,使用場景和示例可以參考上面的說明

程式碼定義

/**
 * Used to enumerate attribute-to-setter renaming. By default, an attribute is associated with
 * setAttribute setter. If there is a simple rename, enumerate them in an array of
 * {@link BindingMethod} annotations in the value.
 */
@Target({ElementType.TYPE})
public @interface BindingMethods {
    BindingMethod[] value();
}複製程式碼

以上就是關於android data-binding library中的定義的註解的重點學習,主要是結合註解的定義進行簡單的實踐,包括使用的示例,使用場景以及自己的踩坑心得。

當然相對於這些註解,其實更加核心的部分是data-binding library 針對這些註解的所定義的註解處理器的核心實現,關於這部分的解析,我會在後面的文章中繼續分析。

參考文獻:

官方指導文件:Data Binding Library

官方data-binding程式碼倉庫

感謝你寶貴的時間閱讀這篇文章,歡迎大家關注我,也歡迎大家評論一起學習,我的個人主頁淺唱android

相關文章