【Medium 萬贊好文】ViewModel 和 LIveData:模式 + 反模式

原文作者: Jose Alcérreca

原文地址: ViewModels and LiveData: Patterns + AntiPatterns

譯者:秉心說

Typical interaction of entities in an app built with Architecture Components

View 和 ViewModel

分配責任

理想情況下,ViewModel 應該對 Android 世界一無所知。這提升了可測試性,內存泄漏安全性,並且便於模塊化。
通常的做法是保證你的 ViewModel 中沒有導入任何 android.*android.arch.* (譯者注:現在應該再加一個 androidx.lifecycle)除外。
這對 Presenter(MVP) 來說也一樣。

❌ 不要讓 ViewModel 和 Presenter 接觸到 Android 框架中的類

條件語句,循環和通用邏輯應該放在應用的 ViewModel 或者其它層來執行,而不是在 Activity 和 Fragment 中。
View 通常是不進行單元測試的,除非你使用了 Robolectric,所以其中的代碼越少越好。
View 只需要知道如何展示數據以及向 ViewModel/Presenter 發送用戶事件。這叫做 Passive View 模式。

✅ 讓 Activity/Fragment 中的邏輯儘量精簡

ViewModel 中的 View 引用

ViewModel 和 Activity/Fragment
具有不同的作用域。當 Viewmodel 進入 alive 狀態且在運行時,activity 可能位於 生命週期狀態 的任何狀態。
Activitie 和 Fragment 可以在 ViewModel 無感知的情況下被銷燬和重新創建。

ViewModels persist configuration changes

向 ViewModel 傳遞 View(Activity/Fragment) 的引用是一個很大的冒險。假設 ViewModel 請求網絡,稍後返回數據。
若此時 View 的引用已經被銷燬,或者已經成爲一個不可見的 Activity。這將導致內存泄漏,甚至 crash。

❌ 避免在 ViewModel 中持有 View 的引用

在 ViewModel 和 View 中通信的建議方式是觀察者模式,使用 LiveData 或者其他類庫中的可觀察對象。

觀察者模式

在 Android 中設計表示層的一種非常方便的方法是讓 View 觀察和訂閱 ViewModel(中的變化)。
由於 ViewModel 並不知道 Android 的任何東西,所以它也不知道 Android 是如何頻繁的殺死 View 的。
這有如下好處:

  1. ViewModel 在配置變化時保持不變,所以當設備旋轉時不需要再重新請求資源(數據庫或者網絡)。
  2. 當耗時任務執行結束,ViewModel 中的可觀察數據更新了。這個數據是否被觀察並不重要,嘗試更新一個
    不存在的 View 並不會導致空指針異常。
  3. ViewModel 不持有 View 的引用,降低了內存泄漏的風險。
private void subscribeToModel() {
  // Observe product data
  viewModel.getObservableProduct().observe(this, new Observer<Product>() {
      @Override
      public void onChanged(@Nullable Product product) {
        mTitle.setText(product.title);
      }
  });
}

✅ 讓 UI 觀察數據的變化,而不是把數據推送給 UI

胖 ViewModel

無論是什麼讓你選擇分層,這總是一個好主意。如果你的 ViewModel 擁有大量的代碼,承擔了過多的責任,那麼:

  • 移除一部分邏輯到和 ViewModel 具有同樣作用域的地方。這部分將和應用的其他部分進行通信並更新
    ViewModel 持有的 LiveData。
  • 採用 Clean Architecture,添加一個 domain 層。這是一個可測試,易維護的架構。Architecture Blueprints 中有 Clean Architecture 的示例。

✅ 分發責任,如果需要的話,添加 domain 層

使用數據倉庫

應用架構指南 中所說,大部分 App 有多個數據源:

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

在你的應用中擁有一個數據層是一個好主意,它和你的視圖層完全隔離。保持緩存和數據庫與網絡同步的算法並不簡單。建議使用單獨的 Repository 類作爲處理這種複雜性的單一入口點.

如果你有多個不同的數據模型,考慮使用多個 Repository 倉庫。

✅ 添加數據倉庫作爲你的數據的單一入口點。

處理數據狀態

考慮下面這個場景:你正在觀察 ViewModel 暴露出來的一個 LiveData,它包含了需要顯示的列表項。那麼 View 如何區分數據已經加載,網絡錯誤和空集合?

  • 你可以通過 ViewModel 暴露出一個 LiveData<MyDataState>MyDataState 可以包含數據正在加載,已經加載完成,發生錯誤等信息。

  • 你可以將數據包裝在具有狀態和其他元數據(如錯誤消息)的類中。查看示例中的 Resource 類。

✅ 使用包裝類或者另一個 LiveData 來暴露數據的狀態信息

保存 activity 狀態

當 activity 被銷燬或者進程被殺導致 activity 不可見時,重新創建屏幕所需要的信息被稱爲 activity 狀態。屏幕旋轉就是最明顯的例子,如果狀態保存在 ViewModel 中,它就是安全的。

但是,你可能需要在 ViewModel 也不存在的情況下恢復狀態,例如當操作系統由於資源緊張殺掉你的進程時。

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

詳見:ViewModels: Persistence, onSaveInstanceState(), Restoring UI
State and Loaders

Event

Event 指只發生一次的事件。ViewModel 暴露出的是數據,那麼 Event 呢?例如,導航事件或者展示 Snackbar 消息,都是應該只被執行一次的動作。

LiveData 保存和恢復數據,和 Event 的概念並不完全符合。看看具有下面字段的一個 ViewModel:

LiveData<String> snackbarMessage = new MutableLiveData<>();

Activity 開始觀察它,當 ViewModel 結束一個操作時需要更新它的值:

snackbarMessage.setValue("Item saved!");

Activity 接收到了值並且顯示了 SnackBar。顯然就應該是這樣的。

但是,如果用戶旋轉了手機,新的 Activity 被創建並且開始觀察。當對 LiveData 的觀察開始時,新的 Activity 會立即接收到舊的值,導致消息再次被顯示。

與其使用架構組件的庫或者擴展來解決這個問題,不如把它當做設計問題來看。我們建議你把事件當做狀態的一部分。

把事件設計成狀態的一部分。更多細節請閱讀 LiveData with SnackBar,Navigation and other events (the SingleLiveEvent case)

ViewModel 的泄露

得益於方便的連接 UI 層和應用的其他層,響應式編程在 Android 中工作的很高效。LiveData 是這個模式的關鍵組件,你的 Activity 和 Fragment 都會觀察 LiveData 實例。

LiveData 如何與其他組件通信取決於你,要注意內存泄露和邊界情況。如下圖所示,視圖層(Presentation Layer)使用觀察者模式,數據層(Data Layer)使用回調。

Observer pattern in the UI and callbacks in the data layer

當用戶退出應用時,View 不可見了,所以 ViewModel 不需要再被觀察。如果數據倉庫 Repository 是單例模式並且和應用同作用域,那麼直到應用進程被殺死,數據倉庫 Repository 纔會被銷燬。 只有當系統資源不足或者用戶手動殺掉應用這纔會發生。如果數據倉庫 Repository 持有 ViewModel 的回調的引用,那麼 ViewModel 將會發生內存泄露。

The activity is nished but the ViewModel is still around

如果 ViewModel 很輕量,或者保證操作很快就會結束,這種泄露也不是什麼大問題。但是,事實並不總是這樣。理想情況下,只要沒有被 View 觀察了,ViewModel 就應該被釋放。

你可以選擇下面幾種方式來達成目的:

  • 通過 ViewModel.onCLeared() 通知數據倉庫釋放 ViewModel 的回調
  • 在數據倉庫 Repository 中使用 弱引用 ,或者 Event Bu(兩者都容易被誤用,甚至被認爲是有害的)。
  • 通過在 View 和 ViewModel 中使用 LiveData 的方式,在數據倉庫和 ViewModel 之間進程通信

✅ 考慮邊界情況,內存泄露和耗時任務會如何影響架構中的實例。

❌ 不要在 ViewModel 中進行保存狀態或者數據相關的核心邏輯。 ViewModel 中的每一次調用都可能是最後一次操作。

數據倉庫中的 LiveData

爲了避免 ViewModel 泄露和回調地獄,數據倉庫應該被這樣觀察:

當 ViewModel 被清除,或者 View 的生命週期結束,訂閱也會被清除:

如果你嘗試這種方式的話會遇到一個問題:如果不訪問 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(repoId) 的調用上。

無論什麼時候你在 ViewModel 內部需要一個 LifeCycle 對象時,Transformation 都是一個好方案。

繼承 LiveData

在 ViewModel 中使用 LiveData 最常用的就是 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 什麼時候開始加載數據。

分割線

翻譯就到這裏了,其實這篇文章已經在我的收藏夾裏躺了很久了。
最近 Google 重寫了 Plaid 應用,用上了一系列最新技術棧, AAC,MVVM, Kotlin,協程 等等。這也是我很喜歡的一套技術棧,之前基於此開源了 Wanandroid 應用 ,詳見 真香!Kotlin+MVVM+LiveData+協程 打造 Wanandroid!

當時基於對 MVVM 的淺薄理解寫了一套自認爲是 MVVM 的 MVVM 架構,在閱讀一些關於架構的文章,以及 Plaid 源碼之後,發現了自己的 MVVM 的一些認知誤區。後續會對 Wanandroid 應用進行合理改造,並結合上面譯文中提到的知識點作一定的說明。歡迎 Star !

文章首發微信公衆號: 秉心說 , 專注 Java 、 Android 原創知識分享,LeetCode 題解。

更多最新原創文章,掃碼關注我吧!

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