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

秉心說TM發表於2019-10-21

原文作者: 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 或者其他類庫中的可觀察物件。

觀察者模式

【Medium 萬贊好文】ViewModel 和 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 就應該被釋放。

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

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

  • 通過 ViewModel.onCLeared() 通知資料倉儲釋放 ViewModel 的回撥
  • 在資料倉儲 Repository 中使用 弱引用 ,或者 Event Bu(兩者都容易被誤用,甚至被認為是有害的)。
  • 通過在 View 和 ViewModel 中使用 LiveData 的方式,在資料倉儲和 ViewModel 之間程式通訊

✅ 考慮邊界情況,記憶體洩露和耗時任務會如何影響架構中的例項。

❌ 不要在 ViewModel 中進行儲存狀態或者資料相關的核心邏輯。 ViewModel 中的每一次呼叫都可能是最後一次操作。

資料倉儲中的 LiveData

為了避免 ViewModel 洩露和回撥地獄,資料倉儲應該被這樣觀察:

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

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

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

如果你嘗試這種方式的話會遇到一個問題:如果不訪問 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 !

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

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

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

相關文章