ViewModel 和 LiveData:為設計模式打 Call 還是唱反調?

Android_開發者發表於2017-11-10

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 是多麼頻繁的幹掉檢視層的小夥伴。這樣有幾個好處:

  1. ViewModel 在配置重新載入(比如螢幕旋轉)的時候是不會變化的,所以沒有必要從外部(比如網路和資料庫)重新獲取資料。
  2. 當耗時操作結束後,ViewModel 中的“被觀察者”被更新,無論這些資料當前有沒有觀察者。這樣不會有嘗試直接更新不存在的檢視的情況,也就不會有 NullPointerException
  3. 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 有多個資料來源,比如:

  1. 遠端:網路、雲端
  2. 本地:資料庫、檔案
  3. 記憶體中的快取

在應用中放一個資料層是一個好主意,資料層完全不關心展示層(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 開始被觀察才載入資料。一些通用的模式是這樣的:

❌ 通常不用擴充 LiveData。可以讓 Activity 或 Fragment 告訴 ViewModel 什麼時候開始載入資料。

[^是否需要關於 Architecture Component 的其他任何主題的指導(或意見)?留下評論!]:

感謝 Lyla FujiwaraDaniel GalpinWojtek KalicińskiFlorina Muntenescu


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章