Android MVVM模式

Google 2015開發者大會終於來了,其中只有一個開發工具真的讓我興奮。我們看到了一系列的改善,例如Android M和以用戶爲中心的特性,Android Studio支持NDK(C/C++),矢量圖,heap分析,改進的主題和layout編輯器,Gradle性能改善,等等。我高興的是我們終於有了一個 Design Support Library 可以實現 Material Design UI patterns 。但大多數這些都已經被其他社區工具和庫實現。

有一件事是社區渴望的,但是還沒有一個好的模式或工具,那就是如何在我們的項目中改進model和views相互協調的代碼。直到現在,Activities和Fragment 通常包含許多脆弱的,不可檢測的和乏味的與views關聯的代碼。但 Data Binding Library 改變了這一切。

自從這個新的庫被公佈,我就在工作中用到了,並深入的去發現它能做什麼和不能做什麼和你能做什麼和不能做什麼。我將目前學到的東西分享出來了,我也希望聽到你的分享。

目標和效率

我們應該對這個庫感興趣因爲它允許我們用更多的方法處理views。這有助於去除那些索然無味的代碼,而伴隨着這些代碼的很可能是與UI相關的bug。更少的代碼意味着更少的錯誤,對吧?對的。

我的另一個更大的目標和某些社區需要的是一致的,那就是降低view和應用邏輯的單元測試消耗。測試總歸是可以測試點,但它是如此艱難,需要這麼多額外的工作,很多人(當然不是我)就跳過吧。這是我們做的更好的機會。

MVVM

在這個library官方文檔中,提供了一個例子直接綁定一個實體類User到layout的一個屬性上。他們給你一個想法,你可以想象下邊這樣:

android:visibility="<strong>@{age &lt; 13 ? View.GONE : View.VISIBLE}</strong>"

但是我們這裏要弄清楚:我沒有推薦實體類直接綁定或者將邏輯放入這些綁定的layout文件中。如果你做了這些事情會更難測試view邏輯和調試。我們是相反的,更容易的測試和調試。

這是MVVM模式到來的情景。簡化事情,通過在引進一個在綁定View和響應事件之間的ViewModel層解耦。ViewModel將是一個POJO(簡單的面向對象)並且包含了和view相關的所有邏輯,使得 更容易的測試 和調試。在這個模式中,綁定將僅僅是一個ViewModel方法結果和View屬性設置的一對一的映射。此外,這使測試和調試view在單元測試中是邏輯簡單和可以實現的。

項目設置

讓我們開始吧。注意:我喜歡簡潔或許會遺漏一些有用信息,所以我建議以 官方文檔 作爲參考。

增加這些依賴到你項目的 build.gradle 中:

classpath 'com.android.databinding:dataBinder:1.0-rc1'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'

然後再你的app module的 build.gradle 文件中增加這些;

apply plugin: 'com.android.databinding'
apply plugin: 'com.neenbedankt.android-apt'

dependencies {
  apt 'com.android.databinding:compiler:1.0-rc1'
}

注意 :如果你有任何像dagger-compiler provided 依賴項,你爲了阻止他們被添加到你的classpath中需要改變 provided 爲 apt

官方文檔中沒有提到 android-apt ,但你會需要的。android-apt插件會使Android Studio在編譯時知道生成的類。當試圖調試問題時,這是至關重要的,瞭解更多綁定機制是如何工作的。

綁定設置

用來接受初始化值和更新的view和接受來自View處理事件的ViewMdel是通過綁定實現的。Data Binding Library自動生成一個綁定類替你處理大部分的繁重工作。讓我們看看綁定發生的細節。

變量聲明

在你的layout文件中,你需要添加一個新的頂層 layout 封裝你已經存在的佈局結構。其中第一個元素是一個 data 元素,包含了在你的佈局綁定中用到的所有類型。

<?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.example.viewmodels.CustomerViewModel" />
    </data>
    ...
    <!-- the rest of your original layout here -->
</layout>

這裏我們聲明瞭一個 viewModel 變量,我們將稍後設置爲我們Fragment的一個特定實例。

綁定聲明

我們可以使用這個 viewModel 做許多有趣的事情,它的內容綁定到layout的widget屬性

<EditText
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:hint="@string/customer_name"
  android:inputType="textCapWords"
  android:text="@{viewModel.customerName}"
  app:enabled="@{viewModel.primaryInfoEnabled}"
  app:error="@{viewModel.nameError}"
  app:addTextChangedListener="@{viewModel.nameWatcher}"
  app:onFocusChangeListener="@{viewModel.nameFocusListener}" />

這裏我們綁定了text值,enable狀態,錯誤信息,text改變監聽事件和焦點改變的監聽事件。

注意 : android 命名空間可以在view的任何標準xml中使用。但是 app 命名空間必須在沒有相對應的xml屬性時被使用。同樣,使用 app 命名空間代替 android 標準屬性看起來像在IDE中去掉錯誤高亮提示。

警告:由於綁定代碼產生的順序,在生成的綁定代碼中你應使用 text 屬性的android 命名空間預防排序問題。否則,在setError()後發生setText()的明顯錯誤。

ViewModel實現

現在讓我們看下ViewModel中相對應的方法。ViewModel繼承了 BaseObservable(這不是必須的,但它幫你節省了很多工作),公共方法的名字是layout要綁定的並且返回類型要與view的setter方法的類型一致。

public class CustomerViewModel extends BaseObservable {

  public String getCustomerName() {
    return customer.getName();
  }

  public boolean isPrimaryInfoEnabled() {
    return editMode && !customer.isVerified();
  }

  @Bindable
  public String getNameError() {
    if (customer.getName().isEmpty()) {
      return "Must enter a customer name";
    }
    return null;
  }

  public TextWatcher getNameWatcher() {
    return new SimpleTextWatcher() {
      @Override
      public void onTextChanged(String text) {
        customer.setName(text);
      }
    };
  }

  public EditText.OnFocusChangeListener getNameFocusListener() {
    return (v, hasFocus) -> {
      if (!hasFocus) notifyPropertyChanged(BR.nameError);
    };
  }
}

第一個方法僅僅返回了一個值。第二個和第三個做了運算確定返回值。其餘的觀察者或監聽者對view的改變做出反應。最偉大的是EditText被ViewModel的值自動填充,如果沒有通過條件的驗證顯示一個錯誤並給ViewModel發送一個事件改變的更新通知。

通知

注意上邊的 validate() 方法。這個監聽調用 notifyPropertyChanged(…) 。這將觸發view重新綁定這個屬性和可能顯示一個然後返回錯誤。 BR 類是爲你生成的,好像R文件。允許你在代碼中引用可綁定的屬性。更詳細的通知不是可能的除非用@Bindable 註釋屬性。因爲我在layout中僅指定了viewModel變量,它默認創建了唯一“bindable”值。

你也可以通過使用通用的 notifyChange() 方法觸發view重新綁定所有的屬性。

小心的是,當TextWatcher調用了 notifyChange() 會進入引起text重複調用的情況,哪個調用TextWatcher,哪個引起 notifyChange() ,你看看會發生什麼。

這看起來像一個有下列情形的最好練習:

  • 短路的通知週期將被查看,是否值在通知前發生了變更。

  • 避免在view的自己通知自己發生了變更。如果其他view需要在這個情況下被通知,你需要綁定和通知再更細的級別

統一處理。

到目前爲止,我們建立的說明都相互做出了反應,做了對的事情。剩下的事情就是引導綁定途徑。這個將在你的Activity或Fragment中。自從我用Fragment加載所有view,我將像這個樣子。

FragmentCustomerBinding binding = DataBindingUtil.bind(view);
viewModel.init(this, customerId);
binding.setViewModel(viewModel);

設想這裏有一個添加了依賴的 viewModel 實例,並且知道如何通過ID進行加載Cusromer。我也通過這實現了一個監聽接口,當Activity相關聯的事情發生時fragment會被通知,像使用了FragmentManager,發送了一個Intent,結束當前Activity,等等。

更進一步的說

我們看到了創建UI的基本構成反應ViewModel同時在變。因爲你沒有在更新UI浪費時間,你能花費時間做:

  • 利用ViewModel有效性開啓或關閉按鍵狀態。
  • 在Viewmodel中基於工作的完成顯示或隱藏加載進度條。
  • 在你的邏輯的侷限性的各個方面運用單元測試。

這裏仍有一些不完善的地方,比如,你不能很容易的綁定ActionBar給ViewModel(有可能放棄了舊的ActionBar接口而直接使用了Toolbar?)

我們在回到需要Activity Context的特定環境下。你能在ViewModel實現接口或設置Activity/Fragment作爲一個監聽。或者就是在Fragment中使用ViewModel和調用它的方法。每一種方法都能用ViewModel實現你的UI邏輯。

試想下現在Fragment你寫的綁定代碼,之前是什麼樣的,節省下來的時間,你可以用在ViewModel的 自動化測試 上面

缺少了什麼

這個庫工作的很好但現在仍然是測試版。我期待它成熟並且提供更好的開發體驗。這些是我想看到的:

  • CMD+B layout頁面中的方法導航快捷鍵可以在ViewMode中使用
  • 出錯的時候有更清晰的錯誤信息
  • layout中的自動完成和類型檢查
  • 通過雙向綁定來簡化引用

  • 綁定支持AdapterViews


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