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

来源:https://www.cnblogs.com/bingxinshuo/archive/2019/10/21/11717178.html
-Advertisement-
Play Games

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


原文作者: Jose Alcérreca

原文地址: ViewModels and LiveData: Patterns + AntiPatterns

譯者:秉心說

Typical interaction of entities in an app built with Architecture Components

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

ViewModels persist configuration changes

向 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)使用回調。

Observer pattern in the UI and callbacks in the data layer

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

The activity is nished but the ViewModel is still around

如果 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 被觀察。下麵這些通用的設計模式:

你並不需要經常繼承 LiveData 。讓 Activity 和 Fragment 告訴 ViewModel 什麼時候開始載入數據。

分割線

翻譯就到這裡了,其實這篇文章已經在我的收藏夾里躺了很久了。
最近 Google 重寫了 Plaid 應用,用上了一系列最新技術棧, AAC,MVVM, Kotlin,協程 等等。這也是我很喜歡的一套技術棧,之前基於此開源了 Wanandroid 應用 ,詳見 真香!Kotlin+MVVM+LiveData+協程 打造 Wanandroid!

當時基於對 MVVM 的淺薄理解寫了一套自認為是 MVVM 的 MVVM 架構,在閱讀一些關於架構的文章,以及 Plaid 源碼之後,發現了自己的 MVVM 的一些認知誤區。後續會對 Wanandroid 應用進行合理改造,並結合上面譯文中提到的知識點作一定的說明。歡迎 Star !

文章首發微信公眾號: 秉心說 , 專註 Java 、 Android 原創知識分享,LeetCode 題解。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 索引的主要作用是起到約束和加速查找,ORM框架(sqlalchemy)是用類和對象對資料庫進行操作 索引的種類 按種類去分 1.普通索引:能夠加速查找 2.主鍵索引:能夠加速查找、不能為空、不能重覆 3.唯一索引:加速查找、可以為空、不能重覆 4.聯合索引(多列): ①聯合主鍵索引 ②聯合唯一 ...
  • [20191013]oracle number類型存儲轉化腳本.txt--//測試看看是否可以利用bc obase=100的輸出解決問題。另外以前腳本忘記考慮尾數的四捨五入問題。--//也許編程就是這樣,總有一些細節沒有考慮到...--//代碼如下num2raw_5.sh:#! /bin/bash# ...
  • 錯誤內容 解決方法 本人連接的是mysql資料庫,檢查sql語法並無錯誤。而此處為 多條query語句,報錯信息指向分號後的第二條語句。 判斷可能是連接的datasource預設只允許執行單個query語句。 在連接datasource的url後面增加參數allowMultiQueries=true ...
  • 停止命令:net stop mysql 啟動命令:net start mysql mysql登錄命令 mysql -h ip -P 埠 -u 用戶名 -p mysql --version 或者mysql -V用於在未登錄情況下,查看本機mysql版本 select version();:登錄情況下 ...
  • 資源列表: "Redis 命令參考" "Commands" Redis是什麼 是一個開源( )的記憶體中的數據結構存儲,用作資料庫、緩存和消息中間件。它支持多種數據結構,如 、`哈希表 列表 無序集合 有序集合 點陣圖 基數統計 地理空間索引 複製 Lua腳本 LRU回收 事務 磁碟持久化 哨兵 自動分 ...
  • 資料庫查詢相信很多人都不陌生,所有經常有人調侃程式員就是CRUD專員,這所謂的CRUD指的就是資料庫的增刪改查。 在資料庫的增刪改查操作中,使用最頻繁的就是查詢操作。而在所有查詢操作中,統計數量操作更是經常被用到。 關於資料庫中行數統計,無論是MySQL還是Oracle,都有一個函數可以使用,那就是 ...
  • 全文檢索技術被廣泛的應用於搜索引擎,查詢檢索等領域。我們在網路上的大部分搜索服務都用到了全文檢索技術。 對於數據量大、數據結構不固定的數據可採用全文檢索方式搜索,比如百度、Google等搜索引擎、論壇站內搜索、電商網站站內搜索等。 什麼是全文檢索呢?先看一下百度百科的專業定義。 為了能更好的理解,我 ...
  • 前言 你還記得是哪一年的 Google IO 正式宣佈 成為 Android 一級開發語言嗎?是 。如今兩年時間過去了,站在一名 Android 開發者的角度來看,Kotlin 的生態環境越來越好了,相關的開源項目和學習資料也日漸豐富,身邊願意去使用或者試用 Kotlin 的朋友也變多了。常年混跡掘 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...