1. 程式人生 > >安卓 Data Binding 使用方法總結(姐姐篇)

安卓 Data Binding 使用方法總結(姐姐篇)

0. 前言

在專案中使用到了 Data Binding,總結使用經驗後寫成本文。
本文涉及安卓自帶框架 DataBinding 的基礎使用方法,適合初次接觸 Data Binding 的同學閱讀。

1. Data Binding 利弊

優勢

DataBinding 出現以前,我們在實現 UI 介面時,不可避免的編寫大量的毫無營養的程式碼:比如 View.findViewById();比如各種更新 View 屬性的 setter:setText()setVisibility()setEnabled() 或者 setOnClickListener() 等等。

這些“垃圾程式碼”數量越多,越容易滋生 bug。

使用 DataBinding,我們可以避免書寫這些“垃圾程式碼”。

劣勢

使用 Data Binding 會增加編譯出的 apk 檔案的類數量和方法數量。

新建一個空的工程,統計開啟 build.gradle 中 Data Binding 開關前後的 apk 檔案中類數量和方法數量,類增加了 120+,方法數增加了 9k+(開啟混淆後該數量減少為 3k+)。

如果工程對方法數量很敏感的話,請慎重使用 Data Binding。

2. 怎麼使用 DataBinding

2.0 配置 build.gradle

Gradle 1.5 alpha 及以上自帶支援 DataBinding,僅需在使用 DataBinding 的 module 裡面的 build.gradle 裡面加上配置即可:

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


在詳細學習 DataBinding 之前,我們可以先來看下一個簡單的例子:LoginDemo4DataBinding。該例子使用非 DataBinding 技術和 DataBinding 技術 2 種方式,實現了一個簡單的登入頁面。通過該例子,我們可以直觀的感受下 DataBinding 的不同。

下面我們詳細討論 DataBinding 的使用方法,下面的例子實現的是在介面上顯示兩個文字:姓氏和名字。

2.1 公共程式碼

公用的 Activity 如下:

public class DataBindingActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TestDbBinding binding = DataBindingUtil.setContentView(this, R.layout.test_db_layout);

        // viewmodel
        UserViewModel viewModel = new UserViewModel();
        binding.setUser(viewModel);

    }
}

其中,TestDbBinding 是根據 R.layout.test_db_layout 自動生成的,binding.setUser() 方法也是根據 layout 中 variable name 自動生成的。

公用的 R.layout.test_db_layout 如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="userViewModel" type="com.example.UserViewModel"/>
   </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="@{userViewModel.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{userViewModel.lastName}"/>
   </LinearLayout>
</layout>

做過 J2EE 開發的同學,有沒有似曾相識感覺?感人感覺,不管是 Data Binding,還是 React Native,都是將 Web 開發的先進思想或技術引進到移動開發領域的一種嘗試。更具體的說,是將宣告式程式設計(Declarative programming,如 JavaScript,CSS 等)引入指令式程式設計(Imperative programming,如 Java 等)中。

 <div id="welcome">Welcome <a href="#">{user.username}</a> <a href="{site}home/logout">logout</a></div>
        <div class="clear"></div>
        {if:isset(menus)}
        {if:menus}
        <div id="moduleList">
            <ul>
                {foreach:menus,$menu}
                <li {if:$menu.is_active} class="current" {end}><div><a href="{site}{$menu.m_uri}/">{__($menu.m_label)}</a></div></li>
                {end}
            </ul>
        </div>

2.2 僅作靜態展示

如果 UI 比較簡單,介面僅僅是靜態展示,不涉及 UI 的動態更新,以下程式碼就能滿足需求了。

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

即使在複雜的 UI 介面中,多數介面元素僅是靜態展示,只有少數介面元素才需要根據使用者的操作動態更新。

以微信朋友圈介面為例,一條狀態的使用者頭像、暱稱、內容、時間等這些元素一旦載入成功,就不再改變了;而點贊列表和評論列表是動態改變的。

如何使用 DataBinding 動態更新介面資料呢?

2.3 動態更新資料

有3種方式實現動態更新介面資料:

  • 實現 Observable 介面;
  • 繼承 BaseObservable;
  • 使用 ObservableField;

2.3.0 實現 Observable 介面

由於 Java 不允許多繼承,而允許同時實現多個介面,所以該方法更具有通用性。

2.3.1 繼承 BaseObservable

public class UserViewModel extends BaseObservable {
    private String firstName;
    private String lastName;

    public UserViewModel(String firstname, String lastname) {
        this.firstName = firstname;
        this.lastName = lastname;
    }

    @Bindable
    public String getFirstName() {
        return firstName;
    }

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

    public void setFirstName(String firstname) {
        this.firstName = firstname;
        notifyPropertyChanged(BR.firstName);
    }

    public void setLastName(String lastname) {
        this.lastName = lastname;
        notifyPropertyChanged(BR.lastName);
    }
}

BR.java 是類似 R.java 的資原始檔,是 Binding Resources 的縮寫,由框架自動生成。

注意,BR 中的 id 生成的依據是 @Bindable 修飾的方法名 getXXX(),而非方法體的內容。當在 getXXX() 方法前加 @Bindable 之後, BR.java 中就立即生成常量 BR.xXX。

還有另外一種寫法:

public @Bindable String firstName;

@Bindable 可以放在 public 之前或之後,但是不能放在 String 之後。

這種方式,框架會自動生成 getFirstName() 方法。注意,此時變數的訪問許可權必須是 public

如果 @Bindable 修飾的變數和 @Bindable 修飾的該變數的 getter 方法同時存在,則 getter 方法失效。

上述兩種方式的區別在於,@Bindable 修飾的 get 方法體,不一定是簡單的 return xxx,也可以是複雜的處理過程。例如,介面上顯示的是 displayName,而 displayName 是由 firstName 和 lastName 按一定規則加工生成,改變 firstName 或 lastName 均會導致 displayName 對應的 UI 元素更新。這時,我們可以這麼寫:

    // ...
    private String displayName;

    // ...

    public void setFirstName(String firstName) {
        this.firstName = firstName;

        notifyPropertyChanged(com.mmlovesyy.displaynamedemo.BR.displayName);
    }

    @Bindable
    public String getDisplayName() {
        return firstName + "." + lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;

        notifyPropertyChanged(com.mmlovesyy.displaynamedemo.BR.displayName);
    }

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

而如果使用 public @Bindable String displayName;,由於 get 方法是框架自動生成的,方法體是 return displayName;, 我們將無法做到這種效果。

其實,從這個案例我們可以一窺其動態更新的原理:通過 setLastName() 改變 lastName,並在該方法中通知訂閱者,訂閱者再呼叫 getDisplayName() 方法來代替 layout 檔案中的 user.displayName,

2.2.2 使用 ObservableXXX / ObservableField<T>

public class UserViewModel {
   public final ObservableField<String> firstName = new ObservableField<>();
   public final ObservableField<String> lastName = new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

ObservableByte / ObservableChar / ObservableShort / ObservableInt /ObservableLong / ObservableFloat / ObservableDouble / ObservableBoolean / ObservableParcelable 為基本資料型別;ObservableField<T> 對應應用型別,如 String,Integer 等。

注意,firstName 的操作方法是 get()set()方法,例如要更新 firstName 的值:

firstName.set("linus chen");
age.set(age.get() + 1);

推而廣之,不管是 ObservableInt/ObservableBoolean/ObservableFloat 等基本資料型別,還是ObservableField<T>的變數,只有呼叫其 set() 方法,其繫結的 UI 元素才會更新。這是因為,更新 UI 元素操作(notifyChanged() 方法)是在 set() 方法中觸發的,具體見如下程式碼:

ObservableInt.java

public class ObservableInt extends BaseObservable implements Parcelable, Serializable {

    /**
     * Set the stored value.
     */
    public void set(int value) {
        if (value != mValue) {
            mValue = value;
            notifyChange();
        }
    }
}

ObservableField.java

public class ObservableField<T> extends BaseObservable implements Serializable {

    /**
     * Set the stored value.
     */
    public void set(T value) {
        if (value != mValue) {
            mValue = value;
            notifyChange();
        }
    }
}

3. 動態更新 ViewGroup

前幾節寫的都是比較簡單的使用方法,是在 View 已經確定的情況下更新其屬性(資料、可見性等)。那麼如何更新 ViewGroup 呢,即動態的向 ViewGroup 新增或移除子 View 呢?這是我們需要使用 @BindingAdapter({“bind:attr1”, “bind:attr2”}) 註解。

先簡要介紹一下該註解的使用方法,然後用一個例子來具體說明。
在 layout 中,使用 “app:attr1” 的格式來新增引數,這些引數會被傳遞到 @BindingAdapter 修飾的方法中,方法必須是 public static void 型別。注意其中的 static,這就限制了方法體中使用的變數(基本型別,引用型別,各種 XXXListener等)和方法要麼通過 @BindingAdapter 傳進去的,要麼是 static的。

當你被 static 困擾時,請考慮一下通過 @BindingAdapter 裡面的引數傳進去。

舉個動態生成的 View 的 click 事件和 layout 中 DataBinding 的事件的互動的例子,就是通過引數將 OnClickListener 的例項以引數的形式傳給 static 方法,而例項 onClick() 實際呼叫 ViewModel 中的 handleOnClick() 方法。

舉個例子,效果圖如下,向一個 LinearLayout 中動態新增 TextView:



layout 程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="viewModel"
            type="com.mmlovesyy.bindingadapterdemo.NamesViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">

        <Button
            android:id="@+id/add_btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{viewModel.onClick}"
            android:text="+" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            app:context="@{viewModel.context}"
            app:names="@{viewModel.names}"></LinearLayout>


    </LinearLayout>


</layout>

Activity 程式碼如下:

public class MainActivity extends AppCompatActivity {

    private NamesViewModel viewModel = new NamesViewModel(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setViewModel(viewModel);
    }
}

NamesViewModel 程式碼如下:

public class NamesViewModel {

    public Context context;
    public final ObservableArrayList<String> names = new ObservableArrayList<>();

    public NamesViewModel(Context context) {
        names.add("linus chen");
        names.add("lin xueyan");
        names.add("zhang xiaona");
        names.add("chen lei");
        names.add("liu yuhong");

        this.context = context;
    }

    @BindingAdapter({"bind:names", "bind:context"})
    public static void setNames(ViewGroup linearLayout, ArrayList<String> names, Context context) {

        linearLayout.removeAllViews();

        for (String s : names) {
            TextView t = new TextView(context);
            t.setText(s);
            linearLayout.addView(t);
        }
    }

    public void onClick(View v) {

        int id = v.getId();

        if (id == R.id.add_btn) {
            names.add("yanyu cai");
        }
    }
}

4. 如何除錯

如果編譯出錯,log 日誌的報錯資訊,即出錯的程式碼行是指框架根據 xml 檔案生成的 xxxBinding.java,,由於目前 Android Studio 尚不支援自動定位出錯程式碼行,所以我們要手動去找該檔案。xxxBinding.java 檔案位置:將 AS 切換成 Project 檢視 - 對應的 module(如 app)- build - intermediates - classes - debug - 對應的 package。

然後根據出錯程式碼行,推測對應的 xml 檔案中出錯的位置。

希望以後 Android Studio 能改善這方面的體驗。其實,這也間接要求我們不要在表示式使用複雜的邏輯,越簡單越容易除錯。

5. 工作原理

6. 效能如何

7. 單元測試

這裡要講的單元測試主要是針對 @BindingAdapter 修飾的方法的。

我們可以這麼寫:

public class MyBindingAdapters {
    @BindingAdapter("android:text")
    public static void setText(TextView view, String value) {
        if (isTesting) {
            doTestingStuff(view, value);
        } else {
            TextViewBindingAdapter.setText(view, value);
        }
    }
}

這有點噁心,而且由於 setText() 方法是 static 的,所以它裡面使用的變數或方法都必須是 static 的,即變數 isTestingdoTestingStuff() 都是 static 的,更加不方便,這顯然是面向過程程式設計的方法,不符合 OCP 原則(對擴充套件開放,非修改封閉)。

我們有一種更好的方法來做單元測試。不過首先我們要先來了解 android.databinding.DataBindingComponent 這個介面的用法,弄懂了它的用法,就知道怎麼做單元測試。而且不僅僅可以做單元測試,還有其他用途。

UML 圖:



DataBindingComponent.java

/**
 * This interface is generated during compilation to contain getters for all used instance
 * BindingAdapters. When a BindingAdapter is an instance method, an instance of the class
 * implementing the method must be instantiated. This interface will be generated with a getter
 * for each class with the name get* where * is simple class name of the declaring BindingAdapter
 * class/interface. Name collisions will be resolved by adding a numeric suffix to the getter.
 * <p>
 * An instance of this class may also be passed into static or instance BindingAdapters as the
 * first parameter.
 * <p>
 * If using Dagger 2, the developer should extend this interface and annotate the extended interface
 * as a Component.
 *
 * @see DataBindingUtil#setDefaultComponent(DataBindingComponent)
 * @see DataBindingUtil#inflate(LayoutInflater, int, ViewGroup, boolean, DataBindingComponent)
 * @see DataBindingUtil#bind(View, DataBindingComponent)
 */
public interface DataBindingComponent {
}

這是一個空的介面,沒有宣告任何方法。注意檔案開頭的註釋部分。定義一個抽象類,抽象 @BindingAdapter 修飾的方法,這裡我們使用 setText() 方法作為例子。

MyBindingAdapter.java

public abstract class MyBindingAdapters {

    @BindingAdapter("android:text")
    public abstract void setText(MyDataBindingComponent component, TextView view, String value);
}

再定義兩個 MyBindingAdapters 的子類,分別用於單元測試和實際生產環境:TestBindingAdapters.java 和 ProdBindingAdapters.java:

TestBindingAdapters.java

   private static final String TAG = "TestBindingAdapters";

    @Override
    public void setText(MyDataBindingComponent component, TextView view, String value) {
        // test code
        Log.d("TestBindingAdapters", "SETTEXT INVOKED");
    }

ProdBindingAdapters.java

public class ProdCBindingAdapters extends MyBindingAdapters {
    @Override
    public void setText(MyDataBindingComponent component, TextView view, String value) {
        TextViewBindingAdapter.setText(view, value);
    }
}

注意,這兩個類提供的 @BindingAdapter 修飾的方法都是非 static 的。

我們在

DataBindingUtil.setContentView(Activity activity, int layoutId,DataBindingComponent bindingComponent);

中要使用的是 DataBindingComponent,所以我們還要定義一個 MyDataBindingComponent,及其兩個子類:TestComponent 和 ProdComponent,分別與 TestBindingAdapters 和 ProdBindingAdapters 相對應。

MyDataBindingComponent.java

public interface MyDataBindingComponent extends android.databinding.DataBindingComponent {
    MyBindingAdapters getMyBindingAdapters();
}

TestComponent.java

public class TestComponent implements MyDataBindingComponent {

    private MyBindingAdapters mAdapter = new TestBindingAdapters();

    @Override
    public MyBindingAdapters getMyBindingAdapters() {
        return mAdapter;
    }
}

ProdComponent.java

public class ProdComponent implements MyDataBindingComponent {

    private String color;

    public ProdComponent(String _color) {
        color = _color;
    }

    private MyBindingAdapters mAdapter = new ProdCBindingAdapters();

    @Override
    public MyBindingAdapters getMyBindingAdapters() {
        return mAdapter;
    }

    public String getColor() {
        return color;
    }
}

然後在 MainActivity 中呼叫:

public class MainActivity extends AppCompatActivity {

    private UserViewModel user;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main, new ProdComponent("blue"));
        user = new UserViewModel("linus", "chen");
        binding.setUser(user);
    }

    @BindingAdapter("android:url")
    public static void setColor(ProdComponent component, TextView view, String url) {
        view.setText(component.getColor());
    }
}

注意,@BindingAdapter 修飾的方法包含在類 MyBindingAdapter 中,所以 DataBindingUtil.setContentView(this, R.layout.activity_main, new ProdComponent("blue")); 中的最後一個引數,DataBindingComponent 類中必須包含一個名為 getMyBindingAdapter() 的 getter 方法,遵循本節開頭的檔案註釋中的規定。

這樣,我們使用 DataBindingUtil.setContentView(this, R.layout.activity_main, new TestComponent()); 進行單元測試。

我們還可以看到,DataBindingComponent 中可以書寫其他方法(例如網路下載方法等),供 @BindingAdapter 修飾的方法(不論是 static 或非 static)呼叫。

如果要使用 Dagger2, 則程式碼如下:
Dagger2

    @Module
    public class TestModule {
        @Provides
        public MyBindingAdapters getMyBindingAdapter() {
            return TestBindingAdapter();
        }
    }
    @Component(modlues = TestModule.class)
    public interface TestComponent extends android.databinding.DataBindingComponent {
    }
    DataBindingUtil.setDefaultComponent(DaggerTestComponent.create());

還有一點需要注意的是,無自定義 DataBindingComponent 時框架生成的 ActivityMainBinding.java 相關程式碼:

android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, firstNameUser);

自定義 DataBindingComponent 時生成的相關程式碼:

this.mBindingComponent.getMyBindingAdapters().setText(this.mboundView2, firstNameUser);

對比可以看出,當自定義 DataBindingAdapter 時,框架會自動呼叫自定義的 setText() 方法,而非預設的 TextViewBindingAdapter.setText()。

8. 最佳實踐

至於 DataBinding 怎麼寫才是最好的,可以參考 Google 的這個開源專案:Android Architecture Blueprints [beta],使用 DataBinding、MVP、DataBinding+MVP 等多種方式實現同一個便箋應用,包括其中的單元測試的寫法,都非常值得學習。

9. 一些建議

9.1 表示式儘量簡單

xml 檔案中不要出現業務邏輯,只出現簡單的 UI 相關的表示式;

在 xml 繫結變數時儘量使用 ” 代替 “”,即使如此,轉義依然不可避免:
- ‘&’ –> ‘&amp;(用英文分號替換)’;
- ‘<’ –> ‘&lt;(用英文分號替換)’;
- ‘>’ -> ‘&gt;(用英文分號替換)’;



9.2 Clean 大法好

有時會遇到莫名其妙的檢查錯誤,可嘗試 clean 工程,或無視之;

9.3 關於程式碼結構

關於程式碼結構,使用 ViewModel(如 UserViewModel)+ Model(如 UserModel) + xml 的結構,對點選事件的處理以及 View 的狀態資料(如評論列表是否展開,當前使用者登入資訊等)放在 ViewModel 中,而正常的資料放在 Model 中。

9.4 不要拒絕 findViewById

DataBinding 和 findViewById() 並不是互斥的,即使在使用 DataBinding 的工程中,我們仍然可以根據需要使用之,特別是在動態更新 ViewGroup 的情景中,有時不可避免的要是用該方法。

9.4 關於對 null 的處理

DataBinding 已經對 null 做了處理,我們無需再關心表示式 npe 的問題,例如 binding.setUser(viewModel); 中 viewModel 為 null 時,執行時不會出現 npe。

關於 @BindingMethod
這個註解是用來關聯 SDK 中提供的控制元件的屬性和方法的,這些屬性的名稱和其 setter 不匹配,需要 @BindingMethod 來“牽繩拉線”,以便自動更新屬性的時候呼叫其對應的 setter。具體的使用方法可以參考 android.databinding.adapters.ImageViewBindingAdapter.java。

開發者一般用不到該註解。

10. 更多資料

也許本文不值得一看,但是下面這些資料則不然。