主頁 > 移動端開發 > 【Medium 萬贊好文】ViewModel 和 LIveData:模式 + 反模式

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

2020-09-16 05:09:19 移動端開發

原文作者: 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 被觀察,下面這些通用的設計模式:

  • 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 題解,

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/53685.html

標籤:Android

上一篇:com.android.tools.aapt2.Aapt2Exception: AAPT2 error: check logs for details

下一篇:如何正確的在 Android 上使用協程 ?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more