MVVM陷阱之DataBinding(數據綁定庫)

官方文檔的描述如下:

數據綁定庫是一種支持庫,藉助該庫,您可以使用聲明性格式(而非程序化地)將佈局中的界面組件綁定到應用中的數據源。

佈局通常是使用調用界面框架方法的代碼在 Activity 中定義的。例如,以下代碼調用 findViewById() 來查找 TextView 微件並將其綁定到 viewModel 變量的 userName 屬性:

 TextView textView = findViewById(R.id.sample_text);
 textView.setText(viewModel.getUserName());

以下示例展示瞭如何在佈局文件中使用數據綁定庫將文本直接分配到微件。這樣就無需調用上述任何 Java 代碼。請注意賦值表達式中 @{} 語法的使用:

<TextView
        android:text="@{viewmodel.userName}" />

藉助佈局文件中的綁定組件,您可以移除 Activity 中的許多界面框架調用,使其維護起來更簡單、方便。還可以提高應用性能,並且有助於防止內存泄漏以及避免發生 Null 指針異常。

目前在網絡上聽到過的質疑聲主要有兩點:

  1. 使用數據綁定會影響應用性能。

  2. 數據綁定寫在佈局裏面維護起來會很困難。

針對以上兩點,從上面的引用中可以看出,官方明確說了是可以提高應用的性能,並且會使項目維護起來更方便。

爲什麼會有人提出以上質疑呢?


他們的觀點大抵可以分爲以下兩種:

  1. 數據綁定會生成額外的類進行佈局和數據的綁定

  2. 數據綁定會維護一個綁定數據表 DataBinderMapperImpl

其實從源碼和實現原理上分析,上面的質疑好像並沒有什麼問題,但是仔細一想,針對第一點,其實沒有數據綁定生成額外的類來進行數據綁定,我們自己不是也得寫代碼進行類似 findViewById()然後賦值等操作麼?可以理解爲這裏只是運用了面向對象的單一職責原則進行了很好的封裝。

第二點就更神奇了,這個數據綁定表本身就是爲了提高效率而設計的,用這一丁點的內存(這是一個SparseIntArray而已,size是佈局數量),難道會比平時我們多用一張沒有壓縮過的圖片消耗大嗎?這點內存而帶來的效率提高難道不值得嗎?

補充一點,如果因爲所謂的應用的效率而拒絕使用可以提高開發效率的技術,那爲什麼我們不去用純C寫呢?native的效率不是更高麼?其實我們都知道,對於大多是應用來說,和遊戲或者其他高內存消耗的應用相比,電商類或者工具類的應用對內存的消耗是很小的,在技術的取捨上要分得清主次。

在使用數據綁定之前你要清楚你要拿它來解決什麼問題?

如果只是爲了替代findViewById()那你可以去使用最新抽取出來的視圖綁定(ViewBinding)因爲這個相對於數據綁定來說更輕巧,在DataBinding剛出來的時候並沒有單獨區分數據 or 視圖綁定,這是在新的版本中,爲了更好的設計,將ViewBinding進行了單獨的抽取。

從最新版本的代碼可以看出:

  public abstract class ViewDataBinding extends BaseObservable 
  implements ViewBinding {}

抽象類ViewDataBinding實現了ViewBinding接口,這個名字也是取得恰到好處,View-Data-Binding,其實大家常說的DataBinding是包含ViewBinding的。


說句題外話,如果只是爲了替代findViewById(),並且是使用kotlin開發的話,也可以考慮Kotlin-android-extension,KAE具有和ViewBinding差不多的功能,具體使用方式在此也不介紹了,有興趣的可以去找相關資料。`

爲了實現數據綁定,類似數據驅動架構,MVVM架構等。恭喜你,這時候Databinding是不錯的選擇。


說到這裏又涉及到MVC,MVP,MVVM設計架構的區別,相信大家對這幾個概念都有所瞭解。


首先,明確一點,架構的目的就是爲了提高開發效率,降低維護成本。


利用面向對象的設計原則,對每個模塊的職責進行合理的劃分,爲了讓其他人更好的理解架構設計思想,然後給予每個模塊一個通用的名詞解釋,爲了更好的說清楚數據綁定,這裏就簡單解釋一下目前常見的幾種架構(以Android爲例)。

MVC

  • 型層(Model),負責處理數據邏輯,一般包含數據庫、本地數據、網絡獲取的Bean等組成。

  • 視圖層(View),負責處理視圖顯示,一般由XML佈局承擔此責任,基本組件和自定義View等充當視圖層的補充元素。

  • 控制層(Control),負責處理業務邏輯,一般由Activity、Fragment承擔此責任。

MVP

  • 模型層(Model),負責處理數據邏輯,一般包含數據庫、本地數據、網絡獲取的Bean等組成。

  • 視圖層(View),負責處理視圖顯示,一般由XML佈局承擔此責任,基本組件和自定義View等充當視圖層的補充元素,Activity、Fragment充當視圖層和控制層的粘合劑。

  • Presenter,負責處理業務邏輯,由從原來MVC控制層中抽取出來的Presenter充當控制層(Presenter)。

MVVM

  • 模型層(Model),負責處理數據邏輯,一般包含數據庫、本地數據、網絡獲取的Bean、(這裏我單獨抽取的視圖數據ViewData概念也屬於Model層)等組成。

  • 視圖層(View),負責處理視圖顯示,一般由XML佈局承擔此責任,基本組件和自定義View等充當視圖層的補充元素,Activity、Fragment主要負責視圖層綁定事件觸發,熟練的話也可以直接在XML中綁定觸發事件。

  • ViewModel,通過數據綁定連接View和Model(這裏由ViewData充當視圖模型被綁定到視圖上)實現視圖層和模型層的解藕,事件觸發後通過ViewModel處理業務邏輯,並且通過數據驅動的方式修改視圖數據,而達到間接修改視圖的功能。

注意:ViewModel一定不能持有視圖層的引用,同樣不能持有Context的引用!不然還是MVP!

對於新手來說,看完上面說明,更讓人覺得摸不着頭腦,只是換一個名字而已,最終不還是分三層嗎?視圖層(View)、數據層(Model)、邏輯處理層,這麼簡單的東西,爲什麼搞得很高深莫測的樣子?

參考一下圖示也許你就豁然開朗了:

其實不同的設計架構最終目的還是爲了解耦,實現高內聚低耦合一直是架構師的理想,這種情況下,每個程序員只需要關心自己的模塊就可以了。

  • 就拿MVVM來說,當一個項目足夠大的時候,可能有的人負責界面繪製(XML),有的人負責業務邏輯處理(ViewModel),有的人負責數據邏輯處理(Model)。這時候,每個模塊的人只需要關心自己的邏輯就可以了,而且每個模塊都可以單獨跑Use Case,每個模塊並沒有很強的依賴關係,而且當某個模塊的邏輯變更了並不一定會影響到其他模塊的變更。

有幾點要注意:

1、官方的ViewModel庫並不是實現MVVM架構的必備,MVVM的重點是解藕,通過一定方式解除View和Model的耦合,比如使用數據綁定庫DataBinding。

2、也有不使用DataBinding實現的MVVM嗎?其實也有,比如說第三版的《第一行代碼》中的方式,利用LiveData實現View和Model的解藕,且ViewModel不依賴View和Context,這裏把Activity和Fragment當作View的主體,而我更傾向於把XML當作View的主體,所見即所得,看得到的當成View,會更直觀一點。Activity和Fragment只是當作一個粘合劑,比如進行事件綁定和一些複雜動畫的處理等。所以DataBinding更多的是服務於XML這種View的。

3、ViewModel庫是在DadaBinding庫之後纔有的,ViewModel類旨在以注重生命週期的方式存儲和管理界面相關的數據。ViewModel 類讓數據可在發生屏幕旋轉等配置更改後繼續留存,這樣可以更好的提升用戶體驗和提高應用性能。

上圖說明了Activity經歷屏幕旋轉而後結束的過程中所處的各種生命週期狀態。該圖還在關聯的 Activity生命週期的旁邊顯示了ViewModel的生命週期。此圖表說明了 Activity 的各種狀態。這些基本狀態同樣適用於 Fragment 的生命週期。


4、其實可以從官方的介紹中看出來,官方的ViewModel庫和我們所說的MVVM架構中的ViewModel層並不是等價的東西,ViewModel層不止包含ViewModel數據(我更願意稱爲ViewData),還應該包含視圖模型的邏輯處理。

總的來說,爲了提高開發效率,爲了更好的在大型團隊中協調開發,MVVM是一個不錯的選擇!目前爲止,個人認爲DataBinding、ViewModel,再加上LiveData,是搭建MVVM架構最完美的組合。

    

第一步:要將應用配置爲使用數據綁定,請在應用模塊的 build.gradle 文件中添加 dataBinding 元素,如以下示例所示:

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

注意:即使應用模塊不直接使用數據綁定,也必須爲依賴於使用數據綁定的庫的應用模塊配置數據綁定。

第二步:修改原來的XML佈局文件,在原佈局外層包裹一層layout標籤,並且使用data和variable標籤,添加需要綁定的數據,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data calss="MainBinding">
       <variable name="user" type="com.example.User"/>
   </data>
<!-- 原佈局開始-->
     <LinearLayout 
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName, default=default_value}"/>
   </LinearLayout>
<!-- 原佈局結束-->
</layout>

第三步:設置佈局頁面以及綁定數據到頁面

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

目前爲止,佈局已經和數據進行了綁定,其實這時候和平時我們使用findViewById()然後再setText()差別並不大,精彩的是後面。

上面雖然實現了基本的數據綁定,但是改變原來的數據對象,界面並不會發生改變,這個時候就需要引入一個新的概念,可觀察字段

可觀察性是指一個對象將其數據變化通知給其他對象的能力。通過數據綁定庫,您可以讓對象、字段或集合變爲可觀察。


通過數據綁定,數據對象可在其數據發生更改時通知其他對象,即監聽器。可觀察類有三種不同類型:對象、字段和集合


當其中一個可觀察數據對象綁定到界面並且該數據對象的屬性發生更改時,界面會自動更新。

最早的時候使用ObservableField或者對基本數據類型封裝好的ObservableField,比如ObservableBoolean、ObservableByte、ObservableChar、ObservableInt等,更輕量級的還有自己通過調用notifyPropertyChanged控制數據刷新。

後來LiveData的出現,可以使用LiveData代替ObservableField並獲得更好的生命週期管理。因此這裏主要講講使用LiveData實現Model和View的解藕。

目前這些方式都是支持的,從代碼中可以看出,最終都會通過註冊,並統一由mLocalFieldObservers進行管理。


ObservableField

   protected boolean updateRegistration(int localFieldId, Observable observable) {
      return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
  }

LiveData調用差不多:

 protected boolean updateLiveDataRegistration(int localFieldId, LiveData<?> observable) {
      mInLiveDataRegisterObserver = true;
      try {
          return updateRegistration(localFieldId, observable, CREATE_LIVE_DATA_LISTENER);
      } finally {
          mInLiveDataRegisterObserver = false;
      }
  }

最終都調用到同樣的一個函數:

 protected void registerTo(int localFieldId, Object observable,
          CreateWeakListener listenerCreator) {
      if (observable == null) {
          return;
      }
      WeakListener listener = mLocalFieldObservers[localFieldId];
      if (listener == null) {
          listener = listenerCreator.create(this, localFieldId);
          mLocalFieldObservers[localFieldId] = listener;
          if (mLifecycleOwner != null) {
              listener.setLifecycleOwner(mLifecycleOwner);
          }
      }
      listener.setTarget(observable);
  }

其實說白了就是觀察這模式的靈活運用,最終實現了數據的綁定,對於雙向綁定也沒有什麼特別的,自動生成的代碼會根據我們在佈局中是否設定了雙向綁定,而主動幫我們設置一個InverseBindingListener監聽,XML中格式如下:android:text="@={...}"。

小知識點

  • 可以在XML中使用default(默認值)也可以不用。

  • 可以在data標籤中,通過class屬性指定要生成的DataBinding文件名字。

  • 可以在data標籤中,通過import標籤導入需要使用的類類型。

  • Null 合併運算符

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

這在功能上等效於:

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

要使XML不含語法錯誤,您必須轉義 < 字符。例如:不要寫成 List<String> 形式,而是必須寫成 List&lt;String>。BindingAdapter、InverseBindingAdapter等註解的使用。例如設置一個斜體綁定適配器:

  @BindingAdapter({"strike"})
  public static void setStrike(TextView view, boolean strike) {
      if (strike) {
          view.setPaintFlags(view.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
      } else {
          view.setPaintFlags(view.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
      }
  }

在佈局中使用

<TextView
  ...
  bind:strike="@{true}"
  ...
  />

設置事件監聽

<!-- 在data標籤添加監聽器綁定-->
<variable
        name="onClickListener"
        type="android.view.View.OnClickListener" />
<!-- 在需要綁定點擊事件的控件上添加綁定-->
...
<View android:onClick="@{onClickListener::onClick}"/>
...

 

通過對源碼的分析可以得知,無論是使用ObservableField還是使用LivaData 最終都會在本地屬性觀察者mLocalFieldObservers中註冊監聽,其實數據綁定庫的使用非常的靈活,除了在上面提到的使用方式,還有更多的使用方式,甚至可以在佈局XML中進行邏輯判斷,事件綁定,佈局管理器設定,列表子佈局設定等,可以說是無所不能。

其實正是這種靈活,也遭受了很多使用者的詬病,說很多邏輯寫在XML中,調試困難,維護麻煩,更有甚者,跳出來直接得出DataBinding不能用的結論,這就好比說菜刀能傷人就說菜刀不好一樣。


有問題的不是工具,而是使用工具的方式!

爲了給DataBinding正名,因此總結一些使用原則,分享如下。

  1. 原則一:能不用可觀察變量儘量不要用。

  2. 原則二:多個變量會同時改變的情況儘量使用一個可觀察變量進行包裝。

  3. 原則三:data標籤能少導入一個變量儘量少導入。

  4. 原則四:XML佈局儘量少或者不使用過多的邏輯判斷。

  5. 原則五:避免對一個數據進行多次綁定(有人通過這種方式刷新界面,這個其實和DataBinding的初衷違背了)。

  6. 原則六:嚴格遵守上述五條。

基於以上六條使用原則,目前經過多次迭代,總結出了滿足絕大多數場景的MVVM。

第一步,整個XML使用統一的格式,無論是普通的佈局,還是列表的Item佈局,抑或是include的佈局,都是使用同樣的方式,這樣就可以使用AndroidStudio的File Templates模版功能創建佈局文件了。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
<!-- 用於控制顯示隱藏導入此類-->
       <import type="android.view.View" />
<!-- 用於點擊事件綁定-->
       <variable name="onClickListener" type="android.view.View.OnClickListener" />
<!-- 用於視圖數據綁定-->
       <variable name="viewData" type="com.example.UserViewData"/>
   </data>
<!-- 原佈局開始-->
     <LinearLayout 
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:id="@+id/helloSomeOne"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:onClick="@{onClickListener::onClick}"
           android:text="@{viewData.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:visibility="@{viewData.noLastName?View.GONE:View.VISIBLE}"
           android:text="@{viewData.lastName, default=default_value}"/>
<!-- 包含另一個佈局 並傳遞事件綁定和視圖數據綁定-->
        <include
            android:id="@+id/includeViewId"
            layout="@layout/include_view_layout"
            bind:onClickListener="@{onClickListener}"
            bind:viewData="@{viewData}" />
   </LinearLayout>
<!-- 原佈局結束-->
</layout>

第二步,創建BaseBindActivity和BaseBindFragment,實現底層的數據綁定,以及生命週期設定,以及事件綁定。

public abstract class BaseBindActivity<B extends ViewDataBinding> extends Activity implements  View.OnClickListener {
private B mBinding;

/**
 * 數據綁定
 */
protected abstract <ViewData> ViewData getViewData();

/**
 * 子類提供有binding的資源ID
 */
@LayoutRes
protected abstract int getLayoutID();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mBinding = DataBindingUtil.setContentView(this, getLayoutID());
    if (mBinding != null) {
        mBinding.setLifecycleOwner(this);
        mBinding.setVariable(BR.onClickListener, this);
        mBinding.setVariable(BR.viewData, getViewData());
    } else {
        setContentView(getLayoutID());
    }
}

@Override
public B getBinding() {
    return mBinding;
}
}

這樣子頁面只需要實現簡單的邏輯處理就可以了。BaseBindFragment邏輯類似就不貼代碼了。

第三步,在具體業務Activity中通過 ViewModelProviders獲取ViewModel,並從ViewModel中獲取ViewData,將ViewData綁定到視圖中,子類通過實現 getViewData(),進行綁定操作。

ViewModelProviders.of(this, factory).get(viewModelClass)

第四步,當用戶操作(比如點擊)導致一個事件產生,在具體業務Activity中,通過ViewModel的方法調用業務數據提供方,並實現業務邏輯,業務處理完成後,操作ViewData中的屬性,實現動態更新界面的功能。

很多邏輯具有通用性,我們可以抽取很多模版代碼作爲基類使用,比如說列表的ListAdapter、數據庫Room、數據差分類DiffUtil.ItemCallback、RecyclerView的ViewHolder都可以進行很好的封裝,使用時就會變得很簡單,以後再也不用處理那麼多的Adapter和ViewHolder了。

因爲篇幅所致,這裏就不詳細介紹了,有興趣的可以參考github源碼,源碼裏面的README也有一部分介紹。

github地址

https://github.com/codyer/component/blob/master/app-core/README.md

原鏈接 https://www.jianshu.com/p/741103ba2ff1

關注我獲取更多知識或者投稿

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章