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

原文做者: Jose Alcérreca

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

譯者:秉心說java

Typical interaction of entities in an app built with Architecture Components

View 和 ViewModel

分配責任

理想狀況下,ViewModel 應該對 Android 世界一無所知。這提高了可測試性,內存泄漏安全性,而且便於模塊化。
一般的作法是保證你的 ViewModel 中沒有導入任何 android.*android.arch.* (譯者注:如今應該再加一個 androidx.lifecycle)除外。
這對 Presenter(MVP) 來講也同樣。android

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

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

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

ViewModel 中的 View 引用

ViewModel 和 Activity/Fragment
具備不一樣的做用域。當 Viewmodel 進入 alive 狀態且在運行時,activity 可能位於 生命週期狀態 的任何狀態。
Activitie 和 Fragment 能夠在 ViewModel 無感知的狀況下被銷燬和從新建立。github

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 擁有大量的代碼,承擔了過多的責任,那麼:segmentfault

  • 移除一部分邏輯到和 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](https://medium.com/google-dev...

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 題解。

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

相關文章
相關標籤/搜索