1. 程式人生 > >【Medium 萬贊好文】ViewModel 和 LIveData:模式 + 反模式

【Medium 萬贊好文】ViewModel 和 LIveData:模式 + 反模式

原文作者: 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 的。
這有如下好處:

  1. ViewModel 在配置變化時保持不變,所以當裝置旋轉時不需要再重新請求資源(資料庫或者網路)。
  2. 當耗時任務執行結束,ViewModel 中的可觀察資料更新了。這個資料是否被觀察並不重要,嘗試更新一個
    不存在的 View 並不會導致空指標異常。
  3. 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 有多個數據源:

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

在你的應用中擁有一個數據層是一個好主意,它和你的檢視層完全隔離。保持快取和資料庫與網路同步的演算法並不簡單。建議使用單獨的 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 題解。

更多最新原創文章,掃碼關注我吧!

相關推薦

Medium ViewModel LIveData模式 + 模式

原文作者: Jose Alcérreca 原文地址: ViewModels and LiveData: Patterns + AntiPatterns 譯者:秉心說 View 和 ViewModel 分配責任 理想情況下,ViewModel 應該對 Android 世界一無所知。這提升了可測試性,記憶

淘寶面試題如何充分利用多核CPU,計算很大的List中所有整數的

引用 前幾天在網上看到一個淘寶的面試題:有一個很大的整數list,需要求這個list中所有整數的和,寫一個可以充分利用多核CPU的程式碼,來計算結果。 一:分析題目 從題中可以看到“很大的List”以及“充分利用多核CPU”,這就已經充分告訴我們要採用多執行緒(任務)進行

取什麼主題沉浸式狀態列----統一Title欄高度

新手上路 直接po程式碼(複製的時候記得加上setBar) 先是網上很好找到的“通過設定全屏,設定狀態列透明” /** * 通過設定全屏,設定狀態列透明 * @param activity */ public static void

部落格搬家舊leetcode 771. Jewels and Stones

  今天開通了部落格園 ,之前的部落格就不用了。之後再陸陸續續把之前的博文重新放到這裡來。有標題這個tag的都是搬運的舊部落格的文章。希望在這裡是個新的開始,嘻嘻。      import java.util.Scanner; class Solution { public

博客搬家舊leetcode 771. Jewels and Stones

inf nbsp 嘻嘻 用戶輸入 pan spa 重新 ring expec   今天開通了博客園 ,之前的博客就不用了。之後再陸陸續續把之前的博文重新放到這裏來。有標題這個tag的都是搬運的舊博客的文章。希望在這裏是個新的開始,嘻嘻。    import java.

部落格搬家舊leetcode515. Find Largest Value in Each Tree Row

  注:這是我春招找實習的時候筆試某公司的原題,當時還傻傻的不太會做。 //廣度優先搜尋就可以實現二叉樹每一層的遍歷,通常都用佇列來實現。佇列儲存未被檢測的結點,結點按寬度優先的次序被訪問和進出佇列。 /** * Definition for a binary tree node. * pub

已測試非常Apache+PHP+MySQL環境搭建

source: http://www.cnblogs.com/Yogurshine/archive/2013/05/24/3097343.html 【Frank注:之前已經搭建好php和apache了,這篇的主要目的是連結Apache和mysql】 一: 安裝Apach

檢測軟鍵盤的彈起與隱藏絕對經典,

今天看到社群裡面有人問關於如何檢測軟鍵盤的彈起和隱藏事件。正確處理好軟鍵盤的彈起和隱藏可以大大提升應用的體驗。這一點,“切客優惠”做的很好。   在軟鍵盤彈起後,下面的分享內容自動隱藏,並且在titlebar上,顯示簽到按鈕。這個是一個非常貼心的設計,使用者體檢大

開發也是測試(三)—pytest fixture scope

導讀 不同scope含義 session module class function Scope session 在一次Run或Debug中執行的所有case共享一個session,第一個case開始執行的時候se

unity中事件函式的執行順序超詳細,圖+

在Unity指令碼中,有一些事件函式會在指令碼執行時按預定順序執行。 該執行順序如下所述: 編輯 Reset:Reset是用來初始化指令碼屬性的,在第一次指令碼附在物件上的時候和按下Reset按鈕的時候會被執行。 第一次場景載入 Awake:Awa

「洛谷5017」「NOIP2018」擺渡車DP,經典

min fine 方塊 ostream set 真的 += mat tps 前言 在考場被這個題搞自閉了,那個時候自己是真的太菜了。qwq 現在水平稍微高了一點,就過來切一下這一道\(DP\)經典好題。 附加一個題目鏈接:【洛谷】 正文 雖然題目非常的簡短,但是解法有很多。

學生信息管理系統EOF BOF

少包 cell enter 常常 avi article ast 管理系統 style 敲完學生信息管理系統時,在刪除信息的時候,常常會出現下圖這種錯誤,遇到問題就要解決這個問題。經過查閱理解了記錄集Recordset的EOF和BOF屬性,用這兩個屬性能夠知道記錄集中是

Java並發編程之二十並發新特性—Lock鎖條件變量(含代碼)

ets exc n) 否則 max 長時間 info trace space 簡單使用Lock鎖 Java 5中引入了新的鎖機制——Java.util.concurrent.locks中的顯式的互斥鎖:Lock接口,它提供了比synchronized更加廣泛的鎖

Java並發編程之十八第五篇中volatile意外問題的正確分析解答(含代碼)

深入 規則 rup lis con method 執行 change .text 轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17382679 在《Java並發編程學習筆記之五:volatile變量修

Java並發編程之十六深入Java內存模型——happen-before規則及其對DCL的分析(含代碼)

無需 bit 對象引用 說了 final 緩存 機器 通過 round 轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17348313 happen—before規則介紹 Java語言中有一個“先行發生

Java並發編程之十二線程間通信中notifyAll造成的早期通知問題(含代碼)

data light lan 添加項 article util tool 元素 seconds 轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17229601 如果線程在等待時接到通知,但線程等待的條件

Java並發編程之十九並發新特性—Executor框架與線程池(含代碼)

接口類 容易 20px 了解 大小 執行c 生命周期 schedule p s Executor框架簡介 在Java 5之後,並發編程引入了一堆新的啟動、調度和管理線程的API。Executor框架便是Java 5中引入的,其內部使用了線程池機制,它在java.

Java並發編程之十一線程間通信中notify通知的遺漏(含代碼)

key wait title net fill article 返回 0ms 註意 轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17228213 notify通知的遺漏很容易理解,即threadA還沒

Java並發編程之十七深入Java內存模型—內存操作規則總結

tle 沒有 article 類型 javase 感知 執行引擎 要求 lock 轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17377197 主內存與工作內存 Java內存模型的主要目標是定義程序中

Java並發編程之十四圖文講述同步的另一個重要功能內存可見性

integer sdn 技術 訪問 span 另一個 edi int tro 轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17288243 加鎖(synchronized同步)的功能不僅僅局限於互斥