ViewModel 和 LiveData:爲設計模式打 Call 仍是唱反調?

ViewModel 和 LiveData:爲設計模式打 Call 仍是唱反調?

View 層和 ViewModel 層

分離職責

用 Architecture Components 構建的 APP 中實體的典型交互 html

理想狀況下,ViewModel 不該該知道任何關於 Android 的事情(如Activity、Fragment)。 這樣會大大改善可測試性,有利於模塊化,而且可以減小內存泄漏的風險。一個通用的法則是,你的 ViewModel 中沒有導入像 android.*這樣的包(像 android.arch.* 這樣的除外)。這個經驗也一樣適用於 MVP 模式中的 Presenter 。前端

❌ 不要讓 ViewModel(或Presenter)直接使用 Android 框架內的類java

條件語句、循環和通常的斷定等語句應該在 ViewModel 或者應用程序的其餘層中完成,而不是在 Activity 或 Fragment 裏。視圖層一般是沒有通過單元測試的(除非你用上了 Robolectric),因此在裏面寫的代碼越少越好。View 應該僅僅負責展現數據以及發送各類事件給 ViewModel 或 Presenter。這被稱爲 Passive View 模式。(憂鬱的 View,哈哈哈)react

✅ 保持 Activity 和 Fragment 中的邏輯代碼最小化android

ViewModel 中的 View 引用

ViewModel 的生命週期跟 Activity 和 Fragment 不同。當 ViewModel 正在工做的時候,一個 Activity 可能處於本身 生命週期 的任何狀態。 Activity 和 Fragment 能夠被銷燬而且從新建立, ViewModel 將對此一無所知。ios

ViewModel 對配置的從新加載(好比屏幕旋轉)具備「抗性」 ↑git

把視圖層(Activity 或 Fragment)的引用傳遞給 ViewModel 是有 至關大的風險 的。假設 ViewModel 從網絡請求數據,而後因爲某些問題,數據返回的時候已經滄海桑田了。這時候,ViewModel 引用的視圖層可能已經被銷燬或者不可見了。這將產生內存泄漏甚至引發崩潰。github

❌ 避免在 ViewModel 裏持有視圖層的引用算法

推薦使用觀察者模式做爲 ViewModel 層和 View 層的通訊方式,可使用 LiveData 或者其餘庫中的 Observable 對象做爲被觀察者。數據庫

觀察者模式

一個很方便的設計 Android 應用中的展現層的方法是讓視圖層(Activity 或 Fragment)去觀察 ViewModel 的變化。因爲 ViewModel 對 Android 一無所知,它也就不知道 Android 是多麼頻繁的幹掉視圖層的小夥伴。這樣有幾個好處:

  1. ViewModel 在配置從新加載(好比屏幕旋轉)的時候是不會變化的,因此沒有必要從外部(好比網絡和數據庫)從新獲取數據。
  2. 當耗時操做結束後,ViewModel 中的「被觀察者」被更新,不管這些數據當前有沒有觀察者。這樣不會有嘗試直接更新不存在的視圖的狀況,也就不會有 NullPointerException
  3. ViewModel 不持有視圖層的引用,這大大減小了內存泄漏的風險。
private void subscribeToModel() {
  // Observe product data
  viewModel.getObservableProduct().observe(this, new Observer<Product>() {
      @Override
      public void onChanged(@Nullable Product product) {
        mTitle.setText(product.title);
      }
  });
}複製代碼

Activity / Fragment 中的一個典型「訂閱」案例。

✅ 讓 UI 觀察數據的變化,而不是直接向 UI 推送數據

臃腫的 ViewModel

能減輕你的擔憂的主意必定是個好主意。若是你的 ViewModel 裏代碼太多、承擔了太多職責,試着去:

  • 將一些代碼移到一個和 ViewModel 具備相同生命週期的 Presenter。讓 Presenter 來跟應用的其餘部分進行溝通並更新 ViewModel 中持有的 LiveData。
  • 添加一個 Domain 層,使用 Clean Architecture 架構。 這個架構很方便測試和維護,同時它也有助於快速的脫離主線程。 Architecture Blueprints 裏面有關於 Clean Architecture 的示例。

✅ 把代碼職責分散出去。若是須要的話,加上一個 Domain 層。

使用數據倉庫(Data Repository)

就像 Guide to App Architecture(應用架構指南) 裏說的那樣,大多數 APP 有多個數據源,好比:

  1. 遠程:網絡、雲端
  2. 本地:數據庫、文件
  3. 內存中的緩存

在應用中放一個數據層是一個好主意,數據層徹底不關心展現層(MVP 中的 P)。因爲保持緩存和數據庫與網絡同步的算法一般很瑣碎複雜,因此建議爲每一個倉庫建立一個類做爲處理同步的單一入口。

若是是許多種而且差異很大的數據模型,考慮使用多個數據倉庫。

✅ 添加數據倉庫做爲數據訪問的單一入口。

關於數據狀態

考慮一下這種狀況:你正在觀察一個 ViewModel 暴露出來的 LiveData,它包含了一個待顯示數據的列表。視圖層該如何區分被加載的數據,網絡錯誤和空列表呢?

  • 你能夠從 ViewModel 中暴露出一個 LiveData<MyDataState>MyDataState 可能包含數據是正在加載仍是已經加載成功、失敗的信息。

能夠將類中有狀態和其餘元數據(好比錯誤信息)的數據封裝到一個類。參見示例代碼中的 Resource 類。

✅ 使用一個包裝類或者 LiveData 來暴露狀態信息。

保存 Activity 的狀態

Activity 的狀態是指在 Activity 消失時從新建立屏幕內容所需的信息,Activity 消失意味着被銷燬或進程被終止。旋轉屏幕是最明顯的狀況,咱們已經在 ViewModel 部分提到了。保存在 ViewModel 的狀態是安全的。

可是,你可能須要在其餘 ViewModel 也消失的場景中恢復狀態。例如,當操做系統因資源不足殺死進程時。

爲了高效地保存和恢復 UI 狀態,組合使用 onSaveInstanceState() 和 ViewModel。

這裏有個示例:ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders

事件

咱們管只發生一次的操做叫作事件。 ViewModels 暴露數據,但對於事件怎麼樣呢?例如,導航事件或顯示 Snackbar 消息等應該僅被執行一次的操做。

事件的概念並不能和 LiveData 存取數據的方式完美匹配。來看下面這個從 ViewModel 中取出來的字段:

LiveData<String> snackbarMessage = new MutableLiveData<>();複製代碼

一個 Activity 開始觀察這個字段,ViewModel 完成了一個操做,因此須要更新消息:

snackbarMessage.setValue("Item saved!");複製代碼

顯然,Activity 接收到這個值後會顯示出來一個 SnackBar。

可是,若是用戶旋轉手機,則新的 Activity 被建立並開始觀察這個字段。當對 LiveData 的觀察開始時,Activity 會當即收到已經使用過的值,這將致使消息再次顯示!

在示例中,咱們繼承 LiveData 建立一個叫作 SingleLiveEvent 的類來解決這個問題。它僅僅發送發生在訂閱後的更新,要注意的是這個類只支持一個觀察者。

✅ 使用像 SingleLiveEvent 這樣的 observable 來處理導航欄或者 SnackBar 顯示消息這樣的狀況

ViewModels 的泄漏問題

響應式範例在 Android 中運行良好,它容許在 UI 和應用程序的其餘層之間創建方便的聯繫。 LiveData 是這個架構的關鍵組件,所以一般你的 Activity 和 Fragment 會觀察 LiveData 實例。

ViewModel 如何與其餘組件進行通訊取決於你,但要注意泄漏問題和邊界狀況。看下面這個圖,其中 Presenter 層使用觀察者模式,數據層使用回調:

UI 中的觀察者模式和數據層中的回凋

若是用戶退出 APP,視圖就消失了因此 ViewModel 也沒有觀察者了。若是數據倉庫是個單例或者是和 Application 的生命週期綁定的,這個數據倉庫在進程被殺掉以前都不會被銷燬。這隻會發生在系統須要資源或用戶手動殺死應用程序時,若是數據倉庫在 ViewModel 中持有對回調的引用,ViewModel 將發生暫時的內存泄漏。

Activity 已經被銷燬了可是 ViewModel 還在苟且

若是是一個輕量級 ViewModel 或能夠保證操做快速完成,這個泄漏並非什麼大問題。可是,狀況並不老是這樣。理想狀況下,ViewModels 在沒有任何觀察者的狀況下不該該持有 ViewModel 的引用:

實現這種機制有不少方法:

  • 經過 ViewModel.onCleared() 能夠通知數據倉庫丟掉對 ViewModel 的回凋。
  • 在數據倉庫中可使用 WeakReference 或者直接使用 Event Bus(兩者都很容易被誤用甚至可能會帶來壞處)。
  • 使用 LiveData 在數據倉庫和 ViewModel 中通訊。就像 View 和 ViewModel 之間那樣。

✅ 考慮邊界狀況,泄漏以及長時間的操做會對架構中的實例帶來哪些影響。

❌ 不要將保存原始狀態和數據相關的邏輯放在 ViewModel 中。任何從 ViewModel 所作的調用均可能是數據相關的。

數據倉庫中的 LiveData

爲了不泄露 ViewModel 和回調地獄(嵌套的回凋造成的「箭頭」代碼),能夠像這樣觀察數據倉庫:

當 ViewModel 被移除或者視圖的生命週期結束,訂閱被清除:

若是嘗試這種方法,有個問題:若是沒法訪問 LifecycleOwner ,如何從 ViewModel 中訂閱數據倉庫呢? 使用 Transformations 是個很簡單的解決方法。 Transformations.switchMap 容許你建立響應其餘 LiveData 實例的改變的 LiveData ,它還容許在調用鏈上傳遞觀察者的生命週期信息:

LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
        if (repoId.isEmpty()) {
            return AbsentLiveData.create();
        }
        return repository.loadRepo(repoId);
    }
);複製代碼

在這個例子中,當觸發器獲得一個更新時,該函數被調用而且結果被分發到下游。 當一個 Activity 觀察到repo 時,相同的 LifecycleOwner 將用於 repository.loadRepo(id) 調用。

✅ 當須要在 ViewModel 中須要 Lifecycle 對象時,使用 Transformation 多是個好辦法。

繼承 LiveData

LiveData 最多見的用例是在 ViewModel 中使用 MutableLiveData 而且將它們暴露爲 LiveData 來保證觀察者不會改變他們。

若是你須要更多功能,擴展 LiveData 會讓你知道何時有活躍的觀察者。例如,當想要開始監聽位置或傳感器服務時,這將頗有用。

public class MyLiveData extends LiveData<MyData> {

    public MyLiveData(Context context) {
        // Initialize service
    }

    @Override
    protected void onActive() {
        // Start listening
    }

    @Override
    protected void onInactive() {
        // Stop listening
    }
}複製代碼

何時不應繼承 LiveData

使用 onActive() 來啓動加載數據的服務是能夠的,可是若是你沒有一個很好的理由這樣作的話就不要這樣作,沒有必要非得等到 LiveData 開始被觀察才加載數據。一些通用的模式是這樣的:

❌ 一般不用拓展 LiveData。可讓 Activity 或 Fragment 告訴 ViewModel 何時開始加載數據。

[^是否須要關於 Architecture Component 的其餘任何主題的指導(或意見)?留下評論!]:

感謝 Lyla FujiwaraDaniel GalpinWojtek KalicińskiFlorina Muntenescu


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索