原文作者: Jose Alcérreca
原文地址: ViewModels and LiveData: Patterns + AntiPatterns
譯者:秉心說

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 無感知的情況下被銷毀和重新創建,

向 ViewModel 傳遞 View(Activity/Fragment) 的參考是一個很大的冒險,假設 ViewModel 請求網路,稍后回傳資料,
若此時 View 的參考已經被銷毀,或者已經成為一個不可見的 Activity,這將導致記憶體泄漏,甚至 crash,
? 避免在 ViewModel 中持有 View 的參考
在 ViewModel 和 View 中通信的建議方式是觀察者模式,使用 LiveData 或者其他類別庫中的可觀察物件,
觀察者模式

在 Android 中設計表示層的一種非常方便的方法是讓 View 觀察和訂閱 ViewModel(中的變化),
由于 ViewModel 并不知道 Android 的任何東西,所以它也不知道 Android 是如何頻繁的殺死 View 的,
這有如下好處:
- ViewModel 在配置變化時保持不變,所以當設備旋轉時不需要再重新請求資源(資料庫或者網路),
- 當耗時任務執行結束,ViewModel 中的可觀察資料更新了,這個資料是否被觀察并不重要,嘗試更新一個
不存在的 View 并不會導致空指標例外, - 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 有多個資料源:
- 遠程:網路或者云端
- 本地:資料庫或者檔案
- 記憶體快取
在你的應用中擁有一個資料層是一個好主意,它和你的視圖層完全隔離,保持快取和資料庫與網路同步的演算法并不簡單,建議使用單獨的 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)使用回呼,

當用戶退出應用時,View 不可見了,所以 ViewModel 不需要再被觀察,如果資料倉庫 Repository 是單例模式并且和應用同作用域,那么直到應用行程被殺死,資料倉庫 Repository 才會被銷毀, 只有當系統資源不足或者用戶手動殺掉應用這才會發生,如果資料倉庫 Repository 持有 ViewModel 的回呼的參考,那么 ViewModel 將會發生記憶體泄露,

如果 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 被觀察,下面這些通用的設計模式:
- 給
ViewModel添加start()方法,并盡快呼叫它,[見 Blueprints example] - 設定一個觸發加載的屬性 [見 GithubBrowerExample]
你并不需要經常繼承 LiveData ,讓 Activity 和 Fragment 告訴 ViewModel 什么時候開始加載資料,
分割線
翻譯就到這里了,其實這篇文章已經在我的收藏夾里躺了很久了,
最近 Google 重寫了 Plaid 應用,用上了一系列最新技術堆疊, AAC,MVVM, Kotlin,協程 等等,這也是我很喜歡的一套技術堆疊,之前基于此開源了 Wanandroid 應用 ,詳見 真香!Kotlin+MVVM+LiveData+協程 打造 Wanandroid! ,
當時基于對 MVVM 的淺薄理解寫了一套自認為是 MVVM 的 MVVM 架構,在閱讀一些關于架構的文章,以及 Plaid 原始碼之后,發現了自己的 MVVM 的一些認知誤區,后續會對 Wanandroid 應用進行合理改造,并結合上面譯文中提到的知識點作一定的說明,歡迎 Star !
文章首發微信公眾號:
秉心說, 專注 Java 、 Android 原創知識分享,LeetCode 題解,更多最新原創文章,掃碼關注我吧!

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/53685.html
標籤:Android
上一篇:com.android.tools.aapt2.Aapt2Exception: AAPT2 error: check logs for details
