「Medium 萬贊好文」ViewModel 和 LIveData:模式 + 反模式

原文作者: https://medium.com/@JoseAlcerreca

原文地址: https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54

譯者:秉心說

「Medium 萬贊好文」ViewModel 和 LIveData:模式 + 反模式

View 和 ViewModel

分配責任

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

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

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

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

ViewModel 中的 View 引用

https://developer.android.com/topic/libraries/architecture/viewmodel.html 和 Activity/Fragment具有不同的作用域。當 Viewmodel 進入 alive 狀態且在運行時,activity 可能位於 https://developer.android.com/guide/components/activities/activity-lifecycle.html 的任何狀態。Activitie 和 Fragment 可以在 ViewModel 無感知的情況下被銷燬和重新創建。

「Medium 萬贊好文」ViewModel 和 LIveData:模式 + 反模式

向 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 中的可觀察數據更新了。這個數據是否被觀察並不重要,嘗試更新一個
  3. 不存在的 View 並不會導致空指針異常。
  4. ViewModel 不持有 View 的引用,降低了內存洩漏的風險。
private void subscribeToModel() {
 // Observe product data
 viewModel.getObservableProduct().observe(this, new Observer() {
 @Override
 public void onChanged(@Nullable Product product) {
 mTitle.setText(product.title);
 }
 });
}

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

胖 ViewModel

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

  • 移除一部分邏輯到和 ViewModel 具有同樣作用域的地方。這部分將和應用的其他部分進行通信並更新
  • ViewModel 持有的 LiveData。
  • 採用 https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html,添加一個 domain 層。這是一個可測試,易維護的架構。https://github.com/android/architecture-samples 中有 Clean Architecture 的示例。

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

使用數據倉庫

如 https://developer.android.com/jetpack/docs/guide 中所說,大部分 App 有多個數據源:

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

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

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

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

處理數據狀態

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

  • 你可以通過 ViewModel 暴露出一個 LiveData ,MyDataState 可以包含數據正在加載,已經加載完成,發生錯誤等信息。
  • 你可以將數據包裝在具有狀態和其他元數據(如錯誤消息)的類中。查看示例中的 https://developer.android.com/jetpack/docs/guide#addendum 類。

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

保存 activity 狀態

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

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

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

詳見:[ViewModels: Persistence, onSaveInstanceState(), Restoring UIState and Loaders](https://medium.com/google-developers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090) 。

Event

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

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

LiveData snackbarMessage = new MutableLiveData<>();

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

snackbarMessage.setValue("Item saved!");

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

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

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

把事件設計成狀態的一部分。更多細節請閱讀 https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

ViewModel 的洩露

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

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

「Medium 萬贊好文」ViewModel 和 LIveData:模式 + 反模式

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

「Medium 萬贊好文」ViewModel 和 LIveData:模式 + 反模式

如果 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 訂閱數據倉庫?使用 https://developer.android.com/topic/libraries/architecture/livedata#transform_livedata 可以很方便的解決這個問題。Transformations.switchMap 可以讓你根據一個 LiveData 實例的變化創建新的 LiveData。它還允許你通過調用鏈傳遞觀察者的生命週期信息:

LiveData repo = Transformations.switchMap(repoIdLiveData, repoId -> {
 if (repoId.isEmpty()) {
 return AbsentLiveData.create();
 }
 return repository.loadRepo(repoId);
 }
);

在這個例子中,當觸發更新時,這個函數被調用並且結果被分發到下游。如果一個 Activity 觀察了 repo,那麼同樣的 LifecycleOwner 將被應用在 repository.loadRepo(repoId) 的調用上。

無論什麼時候你在 https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html 內部需要一個 https://developer.android.com/reference/android/arch/lifecycle/Lifecycle.html 對象時,https://developer.android.com/topic/libraries/architecture/livedata#transform_livedata 都是一個好方案。

繼承 LiveData

在 ViewModel 中使用 LiveData 最常用的就是 MutableLiveData,並且將其作為 LiveData 暴露給外部,以保證對觀察者不可變。

如果你需要更多功能,繼承 LiveData 會讓你知道活躍的觀察者。這對你監聽位置或者傳感器服務很有用。

public class MyLiveData extends LiveData {
 public MyLiveData(Context context) {
 // Initialize service
 }
 @Override
 protected void onActive() {
 // Start listening
 }
 @Override
 protected void onInactive() {
 // Stop listening
 }
}

什麼時候不要繼承 LiveData

你也可以通過 onActive() 來開啟服務加載數據。但是除非你有一個很好的理由來說明你不需要等待 LiveData 被觀察。下面這些通用的設計模式:

  • 給 ViewModel 添加 start() 方法,並儘快調用它。https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.java#L64]
  • 設置一個觸發加載的屬性 https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.kt]

你並不需要經常繼承 LiveData 。讓 Activity 和 Fragment 告訴 ViewModel 什麼時候開始加載數據。

分割線

翻譯就到這裡了,其實這篇文章已經在我的收藏夾裡躺了很久了。最近 Google 重寫了 https://github.com/android/plaid 應用,用上了一系列最新技術棧, https://developer.android.com/topic/libraries/architecture/,MVVM, Kotlin,協程 等等。這也是我很喜歡的一套技術棧,之前基於此開源了 https://github.com/lulululbj/wanandroid 應用 ,詳見 https://juejin.im/post/5cb473e66fb9a068af37a6ce 。

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


分享到:


相關文章: