關於使用 Android MVVM + LiveData 模式的一些建議

簡評:MVVM 是谷歌提出的一種 Android 架構模式,結合了 Data Binding 和一些生命周期組件 LiveData 和 ViewModel 等。詳情可以查看谷歌官方樣例庫。

View 和 ViewModels

理論情況下,ViewModel 不需要知道任何關於 Android 的東西。這提供了可測試性,防止內存泄漏和模塊化的好處。一條基本規制是確保在你的 ViewModels 類中沒有任何 android.* 的類導入(android.arch.* 例外)。這點和 Presenter 一樣。

? 別讓 ViewModels(和 Presenters)知道任何關於 Android 框架的類。

條件語句,循環和一般決策應該在 app 的 ViewModels 或其他層次中完成,而不是在 Activity 或 Fragment 中。View 通常不是單元測試的(除非你使用 Robolectric),所以越少代碼越好。View 僅僅應該知道如何展示數據以及發送用戶的事件給 ViewModel(或 Presenter)。這就叫被動視圖模式。

? 在 Activities 和 Fragments 中保持最小化的邏輯

在 ViewModels 中引用 View

ViewModels 和 Activities 或 Fragments 相比有著不同的範圍。當一個 ViewModel 存活並運行時,一個 Activity 可以處於它生命周期的任何一個狀態。在 ViewModel 不知道的情況下,Activities 和 Fragments 可以被再次摧毀和創建。

ViewModels 在配置改變時仍然存在

將 View 的引用(Activity 或 Fragment)傳遞到 ViewModel 是一個嚴重的風險。假設 ViewModel 中請求了網路數據,數據在之後的一段時間返回。此時,View 的引用有可能被回收或者舊的 Activity 不再可見,這樣就產生了內存泄漏,甚至崩潰。

? 避免在 ViewModels 中引用 Views

在 ViewModels 和 Views 中溝通推薦的方式是觀察者模式,利用 LiveData 或者其他庫提供的可觀察的方式。

觀察者模式

通過觀察者模式,在 Android 展示層中讓 View(Activity 或 Fragment)觀察(訂閱改變) ViewModel 顯得非常方便。因為 ViewModel 不需要知道 Android 內容,它也不知道 Android 是如何頻繁地殺死 View 的。好處在於:

  1. ViewModels 在配置改變時依然存在,因此當旋轉屏幕時不需要再次查詢內部數據(資料庫或網路)。
  2. 當一個長時間的操作結束時,在 ViewModel 中可觀察的部分更新了。無論數據是否被觀察了,當嘗試更新不存在的 View 時沒有空指針異常的發生。
  3. ViewModels 不引用 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 觀察它的改變。

臃腫的 ViewModels

如果你的 ViewModel 中放了太多代碼或者太多的職責,可以考慮:

  • 將一些邏輯移動到 presenter,它和 ViewModel 有相同的範圍。它可以和你 app 中的其他部分溝通並更新 ViewModel 中 LiveData 的持有者。
  • 添加一個領域層並採取簡潔的架構,這樣有利於測試和維護,也同樣促進了快速脫離主線程。在架構藍圖中有一個簡潔架構的樣例。

? 必要時分散職責,增加領域層。

使用數據倉庫

在 App 架構指南中,可以看到大多數應用都有多種數據來源,比如:

  1. 遠程:網路或雲
  2. 本地:資料庫或文件
  3. 內存緩存

在你的應用中擁有數據層是一個好主意,展示層完全注意不到。保留緩存、同步資料庫和網路的演算法都是無關緊要的。擁有一個完全隔離的倉庫類作為一個單一的入口來處理這些複雜的事情是非常推薦的。

如果你有多種並且不同的數據模型,考慮添加多種倉庫。

? 添加一個數據倉庫作為訪問數據的單一入口。

處理數據狀態

考慮這個場景:你正在觀察 ViewModel 中暴露的 LiveData,它包含一個展示項目的列表。那麼在數據載入,網路錯誤或者空列表時,視圖該如何呈現這些變化呢?

  • 你可以在 ViewModel 中暴露一個 LiveData<MyDataState>。例如,MyDataState 應該包含那些數據是否正在載入,或者載入成功或載入失敗的信息。

你可以把數據包裹在一個保存了狀態或其他元數據(例如一條錯誤消息)的類中。看看我們樣例中的 Resource 類。

? 使用一些包裹類或另一個 LiveData 來暴露你數據的信息。

保存 Activity 狀態

Activity 狀態是一些當 Activity 消失時(意味著被回收或進程被殺死)你需要重新恢復屏幕的信息。旋轉屏幕是一個最顯著的案例,還好我們有 ViewModel。狀態保存在 ViewModel 中是安全的。

然而在某些場景中,當 ViewModel 也消失時,你可能也需要恢復狀態。比如當操作系統的資源緊缺時,可能會殺死你的進程。

為了高效地保存和恢復 UI 狀態,需要結合持續性,onSaveInstanceState() 以及 ViewModels。

看看例子:ViewModels:持續,onSaveInstanceState(),恢復 UI 狀態和裝載器

事件

一個事件指發送一次的動作。ViewModels 暴露了數據,但什麼是事件呢?例如,導航事件或者展示 Snackbar 消息都是應該只執行一次的動作。

事件的概念並不能很好的展示 LiveData 是如何存儲和恢複數據的。來看一下下面的 ViewModel:

LiveData<String> snackbarMessage = new MutableLiveData<>();

一個 Activity 開始觀察這個數據,並且 ViewModel 完成了一個操作之後它需要更新這條消息:

snackbarMessage.setValue("Item saved!");

Activity 收到這條消息,並展示在 Snackbar 中。這顯然沒毛病。

然而,如果用戶旋轉手機,創建了新的 Activity 並開始觀察。當 LiveData 觀察發生後,Activity 立即收到了舊的值,這時消息再次展示了!

我們擴展了 LiveData,並創建了一個類叫 SingleLiveEvent,作為剛剛問題的解決方案。它僅僅發送訂閱之後出現的更新。注意它只支持一個觀察者。

? 為像導航或 Snackbar 消息等事件使用可觀察的行為如 SingleLiveEvent。

泄露 ViewModels

反應式範例在 Android 中工作得很好,因為它允許在 UI 和應用的其他層次建立方便的連接。LiveData 是這個結構中的關鍵組件,因此通常情況下你的 Activities 和 Fragments 都會觀察一個 LiveData 實例。

ViewModels 和其他組件是如何溝通的取決於你,但要注意泄露和邊界情況。下圖中展示層使用了觀察者模式,數據層使用了回調:

如果用戶退出了應用,View 將會消失,因此 ViewModel 不再被觀察。如果倉庫是一個單例,或者應用範圍的,那麼倉庫將不會回收直到進程被殺死。這隻會在操作系統需要資源或者用戶手動殺死應用時才會發生。如果倉庫保留了 ViewModel 中回調的引用,那麼 ViewModel 就會暫時泄露。

如果 ViewModel 存活或者被分配的操作很快就完成了,那麼這個泄露沒什麼。然而,不是所有的時候都這樣。理想情況下,ViewModels 應該在沒有任何 Views 觀察它們時回收:

你可以採取以下選項來達到這個目的:

  • 使用 ViewModel.onCleared() 你可以告訴倉庫扔掉 ViewModel 的回調。
  • 在倉庫中你可以使用弱引用或者 EventBus(這兩者都容易濫用甚至有害)

? 考慮邊界情況,泄露以及長時間的操作如何影響你架構中的實例。

? 不要把保存清除狀態或者相關的關鍵邏輯放在 ViewModel 中。任何你從 ViewModel 中的調用都可能是最後一次。

在倉庫中的 LiveData

為了避免 ViewModels 的泄露和回調地獄,倉庫可以這樣觀察:

當 ViewModel 被清除時或者當 View 的生命周期結束時,訂閱也被清除了:

如果用這種方式的話有個 catch:如果你不訪問 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 並且隨著repository.loadRepo(id) 的調用相同的 LifecycleOwner 會被使用。

? 無論何時,當你考慮在 ViewModel 中使用生命周期對象時,Transformation 都可能是個解決方案。

繼承 LiveData

LiveData 最普遍的用例就是在 ViewModels 中使用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() 方法,並儘快調用它。[查看藍圖例子]
  • 設置一個啟動負載的屬性[查看 GithubBrowserExample]

? 通常你不用繼承 LiveData。讓你的 Activity 或者 Fragment 來告訴 ViewModel 什麼時候開始載入數據。

原文鏈接:ViewModels and LiveData: Patterns + AntiPatterns

推薦閱讀

  • 用 Kotlin 開發現代 Android 項目 Part 1

極光日報,極光開發者旗下媒體。

每天導讀三篇英文技術文章。


推薦閱讀:

Android Studio連接真機沒反應?
如何在 Android 的 textview 里獲取行數?
過度依賴框架有什麼不好?
有哪些優秀的 Android 應用開源項目、特效、設計資料推薦?

TAG:Android开发 | MVVM | Android |