- 原文地址:ViewModels and LiveData: Patterns + AntiPatterns
- 原文作者:Jose Alcérreca
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:boileryao
- 校對者:Zhiw miguoer
ViewModel 和 LiveData:為設計模式打 Call 還是唱反調?
View 層和 ViewModel 層
分離職責
用 Architecture Components 構建的 APP 中實體的典型互動
理想情況下,ViewModel 不應該知道任何關於 Android 的事情(如Activity、Fragment)。 這樣會大大改善可測試性,有利於模組化,並且能夠減少記憶體洩漏的風險。一個通用的法則是,你的 ViewModel 中沒有匯入像 android.*
這樣的包(像 android.arch.*
這樣的除外)。這個經驗也同樣適用於 MVP 模式中的 Presenter 。
❌ 不要讓 ViewModel(或Presenter)直接使用 Android 框架內的類
條件語句、迴圈和一般的判定等語句應該在 ViewModel 或者應用程式的其他層中完成,而不是在 Activity 或 Fragment 裡。檢視層通常是沒有經過單元測試的(除非你用上了 Robolectric),所以在裡面寫的程式碼越少越好。View 應該僅僅負責展示資料以及傳送各種事件給 ViewModel 或 Presenter。這被稱為 Passive View 模式。(憂鬱的 View,哈哈哈)
✅ 保持 Activity 和 Fragment 中的邏輯程式碼最小化
ViewModel 中的 View 引用
ViewModel 的生命週期跟 Activity 和 Fragment 不一樣。當 ViewModel 正在工作的時候,一個 Activity 可能處於自己 生命週期 的任何狀態。 Activity 和 Fragment 可以被銷燬並且重新建立, ViewModel 將對此一無所知。
ViewModel 對配置的重新載入(比如螢幕旋轉)具有“抗性” ↑
把檢視層(Activity 或 Fragment)的引用傳遞給 ViewModel 是有 相當大的風險 的。假設 ViewModel 從網路請求資料,然後由於某些問題,資料返回的時候已經滄海桑田了。這時候,ViewModel 引用的檢視層可能已經被銷燬或者不可見了。這將產生記憶體洩漏甚至引起崩潰。
❌ 避免在 ViewModel 裡持有檢視層的引用
推薦使用觀察者模式作為 ViewModel 層和 View 層的通訊方式,可以使用 LiveData 或者其他庫中的 Observable 物件作為被觀察者。
觀察者模式
一個很方便的設計 Android 應用中的展示層的方法是讓檢視層(Activity 或 Fragment)去觀察 ViewModel 的變化。由於 ViewModel 對 Android 一無所知,它也就不知道 Android 是多麼頻繁的幹掉檢視層的小夥伴。這樣有幾個好處:
- ViewModel 在配置重新載入(比如螢幕旋轉)的時候是不會變化的,所以沒有必要從外部(比如網路和資料庫)重新獲取資料。
- 當耗時操作結束後,ViewModel 中的“被觀察者”被更新,無論這些資料當前有沒有觀察者。這樣不會有嘗試直接更新不存在的檢視的情況,也就不會有
NullPointerException
。 - ViewModel 不持有檢視層的引用,這大大減少了記憶體洩漏的風險。
private void subscribeToModel() {
// Observe product data
viewModel.getObservableProduct().observe(this, new Observer<Product>() {
@Override
public void onChanged(@Nullable Product product) {
mTitle.setText(product.title);
}
});
}複製程式碼
Activity / Fragment 中的一個典型“訂閱”案例。
✅ 讓 UI 觀察資料的變化,而不是直接向 UI 推送資料
臃腫的 ViewModel
能減輕你的擔心的主意一定是個好主意。如果你的 ViewModel 裡程式碼太多、承擔了太多職責,試著去:
- 將一些程式碼移到一個和 ViewModel 具有相同生命週期的 Presenter。讓 Presenter 來跟應用的其他部分進行溝通並更新 ViewModel 中持有的 LiveData。
- 新增一個 Domain 層,使用 Clean Architecture 架構。 這個架構很方便測試和維護,同時它也有助於快速的脫離主執行緒。 Architecture Blueprints 裡面有關於 Clean Architecture 的示例。
✅ 把程式碼職責分散出去。如果需要的話,加上一個 Domain 層。
使用資料倉儲(Data Repository)
就像 Guide to App Architecture(應用架構指南) 裡說的那樣,大多數 APP 有多個資料來源,比如:
- 遠端:網路、雲端
- 本地:資料庫、檔案
- 記憶體中的快取
在應用中放一個資料層是一個好主意,資料層完全不關心展示層(MVP
中的 P
)。由於保持快取和資料庫與網路同步的演算法通常很瑣碎複雜,所以建議為每個倉庫建立一個類作為處理同步的單一入口。
如果是許多種並且差別很大的資料模型,考慮使用多個資料倉儲。
✅ 新增資料倉儲作為資料訪問的單一入口。
關於資料狀態
考慮一下這種情況:你正在觀察一個 ViewModel 暴露出來的 LiveData,它包含了一個待顯示資料的列表。檢視層該如何區分被載入的資料,網路錯誤和空列表呢?
- 你可以從 ViewModel 中暴露出一個
LiveData<MyDataState>
。MyDataState
可能包含資料是正在載入還是已經載入成功、失敗的資訊。
可以將類中有狀態和其他後設資料(比如錯誤資訊)的資料封裝到一個類。參見示例程式碼中的 Resource 類。
✅ 使用一個包裝類或者 LiveData 來暴露狀態資訊。
儲存 Activity 的狀態
Activity 的狀態是指在 Activity 消失時重新建立螢幕內容所需的資訊,Activity 消失意味著被銷燬或程式被終止。旋轉螢幕是最明顯的情況,我們已經在 ViewModel 部分提到了。儲存在 ViewModel 的狀態是安全的。
但是,你可能需要在其他 ViewModel 也消失的場景中恢復狀態。例如,當作業系統因資源不足殺死程式時。
為了高效地儲存和恢復 UI 狀態,組合使用 onSaveInstanceState()
和 ViewModel。
這裡有個示例:ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders
事件
我們管只發生一次的操作叫做事件。 ViewModels 暴露資料,但對於事件怎麼樣呢?例如,導航事件或顯示 Snackbar 訊息等應該僅被執行一次的操作。
事件的概念並不能和 LiveData 存取資料的方式完美匹配。來看下面這個從 ViewModel 中取出來的欄位:
LiveData<String> snackbarMessage = new MutableLiveData<>();複製程式碼
一個 Activity 開始觀察這個欄位,ViewModel 完成了一個操作,所以需要更新訊息:
snackbarMessage.setValue("Item saved!");複製程式碼
顯然,Activity 接收到這個值後會顯示出來一個 SnackBar。
但是,如果使用者旋轉手機,則新的 Activity 被建立並開始觀察這個欄位。當對 LiveData 的觀察開始時,Activity 會立即收到已經使用過的值,這將導致訊息再次顯示!
在示例中,我們繼承 LiveData 建立一個叫做 SingleLiveEvent 的類來解決這個問題。它僅僅傳送發生在訂閱後的更新,要注意的是這個類只支援一個觀察者。
✅ 使用像 SingleLiveEvent 這樣的 observable 來處理導航欄或者 SnackBar 顯示訊息這樣的情況
ViewModels 的洩漏問題
響應式範例在 Android 中執行良好,它允許在 UI 和應用程式的其他層之間建立方便的聯絡。 LiveData 是這個架構的關鍵元件,因此通常你的 Activity 和 Fragment 會觀察 LiveData 例項。
ViewModel 如何與其他元件進行通訊取決於你,但要注意洩漏問題和邊界情況。看下面這個圖,其中 Presenter 層使用觀察者模式,資料層使用回撥:
UI 中的觀察者模式和資料層中的回凋
如果使用者退出 APP,檢視就消失了所以 ViewModel 也沒有觀察者了。如果資料倉儲是個單例或者是和 Application 的生命週期繫結的,這個資料倉儲在程式被殺掉之前都不會被銷燬。這隻會發生在系統需要資源或使用者手動殺死應用程式時,如果資料倉儲在 ViewModel 中持有對回撥的引用,ViewModel 將發生暫時的記憶體洩漏。
Activity 已經被銷燬了但是 ViewModel 還在苟且
如果是一個輕量級 ViewModel 或可以保證操作快速完成,這個洩漏並不是什麼大問題。但是,情況並不總是這樣。理想情況下,ViewModels 在沒有任何觀察者的情況下不應該持有 ViewModel 的引用:
實現這種機制有很多方法:
- 通過 ViewModel.onCleared() 可以通知資料倉儲丟掉對 ViewModel 的回凋。
- 在資料倉儲中可以使用 WeakReference 或者直接使用 Event Bus(二者都很容易被誤用甚至可能會帶來壞處)。
- 使用 LiveData 在資料倉儲和 ViewModel 中通訊。就像 View 和 ViewModel 之間那樣。
✅ 考慮邊界情況,洩漏以及長時間的操作會對架構中的例項帶來哪些影響。
❌ 不要將儲存原始狀態和資料相關的邏輯放在 ViewModel 中。任何從 ViewModel 所做的呼叫都可能是資料相關的。
資料倉儲中的 LiveData
為了避免洩露 ViewModel 和回撥地獄(巢狀的回凋形成的“箭頭”程式碼),可以像這樣觀察資料倉儲:
當 ViewModel 被移除或者檢視的生命週期結束,訂閱被清除:
如果嘗試這種方法,有個問題:如果無法訪問 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(id)
呼叫。
✅ 當需要在 ViewModel 中需要 Lifecycle 物件時,使用 Transformation 可能是個好辦法。
繼承 LiveData
LiveData 最常見的用例是在 ViewModel 中使用 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 ) - 設定一個控制啟動載入的屬性 (參見 GithubBrowserExample )
❌ 通常不用擴充 LiveData。可以讓 Activity 或 Fragment 告訴 ViewModel 什麼時候開始載入資料。
[^是否需要關於 Architecture Component 的其他任何主題的指導(或意見)?留下評論!]:
感謝 Lyla Fujiwara、Daniel Galpin、Wojtek Kaliciński 和 Florina Muntenescu。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。