ViewModel:持久化、onSaveInstanceState()、UI 狀態恢復和 Loader

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

介紹

我在上篇博文中用新的 ViewModel 類開發了一個簡單的用例來儲存配置更改過程中的籃球分數。ViewModel 被設計用來以與生命週期相關的方式儲存和管理 UI 相關的資料。ViewModel 允許資料在例如螢幕旋轉這樣的配置更改後依然保留。

現在,你可能會有幾個問題是關於 ViewModel 到底能做什麼。本文我將解答:

  • ViewModel 是否對資料進行了持久化? 簡而言之,沒有,還像平常那樣去持久化。
  • ViewModel 是 onSaveInstanceState 的替代品嗎? 簡而言之,不是,但是他們不無關聯,請繼續讀。
  • 我如何高效地使用 ViewModel 來儲存和恢復 UI 狀態? 簡而言之,你可以混合混合 ViewModels、 onSaveInstanceState()、本地持久化一起使用。
  • ViewModel 是 Loader 的一個替代品嗎? 簡而言之,對,ViewModel 結合其他幾個類可以代替 Loader 使用。

圖模型是否對資料進行了持久化?

簡而言之,沒有。 還像平常那樣去持久化。

ViewModel 持有 UI 中的臨時資料,但是他們不會進行持久化。一旦相關聯的 UI 控制器(fragment/activity)被銷燬或者程式停止了,ViewModel 和所有被包含的資料都將被垃圾回收機制標記。

那些被多個應用共用的資料應該像正常那樣通過 本地資料庫,Shared Preferences,和/或者雲端儲存被持久化。如果你想讓使用者在應用執行在後臺三個小時候後再返回到與之前完全相同的狀態,你也需要將資料持久化。這是因為一旦你的活動進入後臺,此時如果你的裝置執行在低記憶體的情況下,你的應用程式是可以被終止的。下面是 activity 類文件中的一個手冊表,它描述了在 activity 的哪個生命週期狀態時你的應用是可被終止的:

Activity 生命週期文件

在此提醒,如果一個應用程式由於資源限制而被終止的話,則不是正常終止並且沒有額外的生命週期回撥。這意味著你不能依賴於 onDestroy 呼叫。在程式終止的時候你沒有機會持久化資料。因此如果你想最大可能的保持資料不丟失,你應該在使用者一進入(activity)的時候就進行持久化。也就是說即便你的應用在由於資源限制而被終止或者裝置電量用完了的時候資料也將會被儲存下來。如果你允許在類似裝置突然關機的情況下丟失資料,你可以在 'onStop()'回撥的時候將其儲存,這個方法在 activity 一進入後臺的時候就會被呼叫。

ViewModel 是 onSaveInstanceState 的替代品嗎?

簡而言之,不是, 但是他們不無關聯,請繼續讀。

理解 onSaveInstanceState()Fragment.setRetainInstance(true) 二者之間的不同有助於理解了解這種差異的微妙之處。

onSaveInstanceState(): 這個回撥是為了儲存兩種情況下的少量 UI 相關的資料:

  • 應用的程式在後臺的時候由於記憶體限制而被終止。
  • 配置更改。

onSaveInstanceState() 是被系統在 activity stopped 但沒有 finished 時呼叫的,而不是在使用者顯式地關閉 activity 或者在其他情形而導致 finish() 被呼叫的時候呼叫。

注意,很多 UI 資料會自動地被儲存和恢復:

“該方法的預設實現儲存了關於 activity 的檢視層次狀態的臨時資訊,例如 EditText 控制元件中的文字或者 ListView 控制元件中的滾動條位置。” — Saving and Restoring Instance State Documentation

這些也是很好的例子說明了 onSaveInstanceState() 方法中儲存的資料的型別。onSaveInstanceState() 不是被設計來儲存類似 bitmap 這樣的大的資料的。onSaveInstanceState() 方法被設計用來儲存那些小的與 UI 相關的並且序列化或者反序列化不復雜的資料。如果被序列化的物件是複雜的話,序列化會消耗大量的記憶體。由於這一過程發生在主執行緒的配置更改期間,它需要快速處理才不會丟幀和引起視覺上的卡頓。

Fragment.setRetainInstance(true)Handling Configuration Changes documentation 描述了在配置更改期間的一個用來儲存資料的程式使用了一個保留的 fragment。這聽起來沒有 onSaveInstanceState() 涵蓋了配置更改和程式關閉兩種情況那麼有用。建立一個保留 fragment 的好處是這可以儲存類似 image 那樣的大型資料集或者網路連線那樣的複雜物件。

ViewModel 只能在配置更改相關的銷燬的情況下保留,而不能在被終止的程式中存留。 這使 ViewModel 成為搭配 setRetainInstance(true)(實際上,ViewModel 在幕後使用了一個 fragment 並將 setRetainInstance 方法中的引數設定為 true) 一塊使用的 fragment 的一種替代品。

ViewModel 的其他好處

ViewModel 和 onSaveInstanceState() 在 UI 資料的儲存方法上有很大差別。onSaveInstanceState() 是生命週期的一個回撥函式,而 ViewModel 從根本上改變了 UI 資料在你的應用中的管理方式。下面是使用了 ViewModel 後比 onSaveInstanceState() 之外的更多的一些好處:

  • ViewModel 鼓勵良好的架構設計。資料與 UI 程式碼分離,這使程式碼更加模組化且簡化了測試。
  • onSaveInstanceState() 被設計用來儲存少量的臨時資料,而不是複雜的物件或者媒體資料列表。一個 ViewModel 可以代理複雜資料的載入,一旦載入完成也可以作為臨時的儲存
  • onSaveInstanceState() 在配置更改期間和 activity 進入後臺時被呼叫;在這兩種情況下,如果你的資料被儲存在 ViewModel 中,實際上並不需要重新載入或者處理他們。

我如何高效地使用 ViewModel 來儲存和恢復 UI 狀態?

簡而言之,你可以混合使用 ViewModelonSaveInstanceState()本地持久化。繼續讀看看如何使用。

重要的是你的 activity 維持著使用者期望的狀態,即便是螢幕旋轉,系統關機或者使用者重啟。如我剛才所說,不要用複雜物件阻塞 onSaveInstanceState 方法同樣也很重要。你也不想在你不需要的時候重新從資料庫載入資料。讓我們看一個 activity 的例子,在這個 activity 中你可以搜尋你的音樂庫:

Activity 未搜尋時及搜尋後的狀態示例。

使用者離開一個 activity 有兩種常用的方式,使用者期望的也是兩種不同的結果:

  • 第一個是使用者是否徹底關閉了 activity。如果使用者將一個 activity 從 recents screen 中滑出或者導航出去或退出一個 activity 就可以徹底關閉它。這兩種情形都假設使用者永久退出了這個 activity,如果重新進入那個 activity,他們所期望的是一個乾淨的頁面。對我們的音樂應用來說,如果使用者完全關閉了音樂搜尋的 activity 然後重新開啟它,音樂搜尋框和搜尋結果都將被清除。
  • 另一方面,如果使用者旋轉手機或者 在activity 進入後臺然後回來,使用者希望搜尋結果和他們想搜尋的音樂仍存在,就像進入後臺前那樣。使用者有數種途徑可以使 activity 進入後臺。他們可以按 home 鍵或者通過應用的其他地方導航(出去)。抑或在檢視搜尋結果的時候電話打了進來或收到通知。然而使用者最終希望的是當他們返回到那個 activity 的時候頁面狀態與離開前完全一樣。

為了實現這兩種情形下的行為,用可以將本地持久化、ViewModel 和 onSaveInstanceState() 一起使用。每一種都會儲存 activity 中使用的不同資料:

  • 本地持久化是用於儲存當開啟或關閉 activity 的時所有你不想丟失的資料。

    舉例: 包含了音訊檔案和後設資料的所有音樂物件的集合。

  • ViewModel 是用於儲存顯示相關 UI 控制器的所需的所有資料。

    舉例: 最近的搜尋結果。

  • onSaveInstanceState 是用於儲存在 UI 控制器被系統終止又重建後可以輕鬆地重新載入 activity 狀態時所需的少量資料。在本地儲存中持久化複雜物件,在 onSaveInstanceState() 中為這些物件儲存唯一的 ID,而不是直接儲存複雜物件。 舉例: 最近的搜尋查詢。

在音樂搜尋的例子中,不同的事件應該被這樣處理:

使用者新增一首音樂的時候 — ViewModel 會迅速代理本地持久化這條資料。如果新新增的音樂需要在 UI 上顯示,你還應該更新 ViewModel 中的資料來反應音樂的新增。謹記切勿在主執行緒中向資料庫插入資料。

當使用者搜尋音樂的時候 — 任何從資料庫為 UI 控制器載入的複雜音樂資料應該馬上存入 ViewModel。你也應該將搜尋查詢本身存入 ViewModel。

當這個 activity 處於後臺並且被系統終止的時候 — 一旦 activity 進入後臺 onSaveInstanceState() 就會被呼叫。你應將搜尋查詢存入 onSaveInstanceState() 的 bundle 裡。這些少量資料易於儲存。這同樣也是使 activity 恢復到當前狀態所需的所有資料。

當 activity 被建立的時候 — 可能出現三種不同的方式:

  • Activity 是第一次被建立:在這種情況下,onSaveInstanceState()方法中的 bundle 裡是沒有資料的,ViewModel 也是空的。建立 ViewModel 時,你傳入一個空查詢,ViewModel 會意識到還沒有資料可以載入。這個 activity 以一種全新的狀態啟動起來。
  • Activity 在被系統終止後建立:activity 的 onSaveInstanceState() 的 bundle 中儲存了查詢。Activity 會將這個查詢傳入 ViewModel。ViewModel發現快取中沒有搜尋結果,就會使用給定的搜尋查詢代理載入搜尋結果。
  • Activity 在配置更改後被建立:Activity 會將本次查詢儲存在 onSaveInstanceState() 的 bundle 引數中並且 ViewModel 也會將搜尋結果快取起來。你通過 onSaveInstanceState() 的 bundle 將查詢傳入 ViewModel,這將決定它已載入了必須的資料從而需要重新查詢資料庫。

這是一個良好的儲存和恢復 activity 狀態的方法。基於你的 activity 的實現,你可能根本不需要 onSaveInstanceState()。例如,有些 activity 在被使用者關閉後不會以一個全新的狀態開啟。一般地,當我在 Android 手機上關閉然後重新開啟 Chrome 時,返回到了關閉 Chrome 之前正在瀏覽的頁面。如果你的 activity 行為如此,你可以不使用 onSaveInstanceState() 而在本地持久化所有資料。同樣以音樂搜尋為例,那意味著在例如 Shared Preferences 中持久化最近的查詢。

此外,當你通過 intent 開啟一個 activity,配置更改和系統恢復這個 activity 時 bundle 引數都會被傳進來。如果搜尋查詢是通過 intent 的 extras 傳進來,那麼你就可以使用 extras 中的 bundle 代替 onSaveInstanceState() 中的 bundle。

不過,在這兩種場景中,你仍需要一個 ViewModel 來避免因配置更改而重新從資料庫中載入資料導致的資源浪費。

ViewModel 是 Loader 的一個替代品嗎?

簡而言之,對,ViewModel 結合其他幾個類可以代替 Loader 使用。

Loader 是 UI 控制器用來載入資料的。此外,Loader 可以在配置更改期間保留,比如說在載入的過程中你旋轉了手機螢幕。這聽起來很耳熟吧!

Loader ,特別是 CursorLoader,的常見用法是觀察資料庫的內容並保持資料與 UI 同步。使用 CursorLoader 後,如果資料庫其中的一個值發生改變,Loader 就會自動觸發資料重新載入並且更新 UI。

ViewModel 與其他架構元件 LiveDataRoom 一起使用可以替代 Loader。ViewModel 保證配置更改後資料不丟失。LiveData 保證 UI 與資料同步更新。Room 確保你的資料庫更新時,LiveData 被通知到。

由於 Loader 在 UI 控制器中作為回撥被實現,因此 ViewModel 的一個額外優點是將 UI 控制器與資料載入分離開來。這可以減少類之間的強引用。

一些使用 ViewModels 、LiveData 為載入資料的方法:

  • 這篇文章中,Ian Lake 概述瞭如何使用 ViewModel 和 LiveData 來代替 AsyncTaskLoader
  • 隨著程式碼變得越來越複雜,你可以考慮在一個單獨的類裡進行實際的資料載入。一個 ViewModel 類的目的是為 UI 控制器持有資料。載入、持久化、管理資料這些複雜的方法超出了 ViewModel 傳統功能的範圍。Guide to Android App Architecture 建議建立一個倉庫類。

“倉庫模組負責處理資料操作。他們為應用的其他部分提供了一套乾淨的 API。當資料更新時他們知道從哪裡獲取資料以及呼叫哪個 API。你可以把他們當做是不同資料來源(持久模型、web service、快取等)之間的協調員。” — Guide to App Architecture

結論以及進一步學習

在本文中,我回答了幾個關於 ViewModel 類是什麼和不是什麼的問題。關鍵點是:

  • ViewModel 不是持久化的替代品 — 當資料改變時像平常那樣持久化他們。
  • ViewModel 不是 onSaveInstanceState() 的替代品,因為他們在與配置更改相關的銷燬時儲存資料,而不能在系統殺死應用程式時儲存。
  • onSaveInstanceState() 並不適用於那些需要長時間序列化/反序列化的資料。
  • 為了高效的儲存和恢復 UI 狀態,可以混合使用 持久化、onSaveInstanceState() 和 ViewModel。複雜資料通過本地持久化儲存然後用 onSaveInstanceState() 來儲存那些複雜資料的唯一 ID。ViewModel 在資料載入後將他們儲存在記憶體中。
  • 在這個場景下,ViewModel 在 activity 旋轉或者進入後臺時仍保留資料,而單純用 onSaveInstanceState() 並沒那麼容易實現。
  • 結合 ViewModel 和 LiveData 一起使用可以代替 Loader。你可以使用 Room 來代替 CursorLoader 的功能。
  • 建立倉庫類來支援一個可伸縮的載入、快取和同步資料的架構。

想要更多 ViewModel 相關的乾貨?請看:

架構元件是基於你反饋來建立的。如果你有關於 ViewModel 或者任何架構元件的問題,請檢視我們的反饋頁面。關於本系列的任何問題,敬請留言。

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


相關文章