本指南說明使用者對 UI 狀態的期望,以及可用於保留狀態的選項。
系統毀損主機活動或應用程式程序後,快速儲存及還原 UI 狀態,對於提供良好的使用者體驗至關重要。使用者會期望 UI 狀態保持不變,但系統可能會刪除代管畫面的 Activity 及其儲存的狀態。
如要消除使用者期望和系統行為之間的落差,請搭配使用下列方法:
ViewModel物件。- 下列情境中的儲存狀態:
- 可組合函式:
rememberSerializable和rememberSaveable。 - ViewModels:
SavedStateHandle。
- 可組合函式:
- 本機儲存空間,可在應用程式和螢幕轉換期間保留 UI 狀態。
最佳解決方案取決於 UI 資料的複雜度、應用程式的用途,以及在資料存取速度和記憶體用量之間取得平衡。
請確保應用程式符合使用者期望,並提供快速回應的介面。避免在將資料載入 UI 時發生延遲,尤其是在旋轉等常見設定變更後。
使用者期望和系統行為
視採取的行動而定,使用者會預期能夠清除或保留 UI 狀態。在某些情況下,系統會自動執行使用者預期的操作。在其他情況下,系統則會執行相反操作。
使用者啟動的使用者介面狀態關閉
使用者預期在前往某個畫面時,系統會暫時維持該畫面的 UI 狀態,直到使用者完全關閉畫面為止。使用者可以透過下列方式完全關閉畫面或應用程式:
- 從「總覽」(近期存取) 畫面滑動以離開應用程式。
- 從「設定」畫面終止或強制結束應用程式。
- 重新啟動裝置。
- 完成某幾種「完成」動作 (由
Activity.finish()提供支援)。
在上述完全關閉的狀況下,使用者的假設是他們已永久離開畫面,如果返回,畫面則是從乾淨的狀態開始。這些關閉情境的系統行為與使用者預期相符:主機活動例項會遭刪除,並從記憶體中移除。此外,在活動中儲存的所有狀態,或是與活動相關的已儲存狀態記錄,也會一併移除。
這項關於完全關閉的規則有部分例外情況,比方說,使用者可能會期望瀏覽器將他們導向使用返回按鈕離開瀏覽器前,當時正在瀏覽的網頁。
系統啟動的使用者介面狀態關閉
在設定變更 (例如旋轉或切換至多視窗模式) 的過程中,使用者會期望畫面的 UI 狀態保持不變。不過,當這類設定變更發生時,系統會刪除主機活動,清除任何儲存在其中的 UI 狀態。如要進一步瞭解裝置設定,請參閱「回應該 Jetpack Compose 中的設定變更」。
請注意,雖然可以覆寫修改設定的預設行為,但不建議這麼做。詳情請參閱「處理設定變更」。
如果暫時切換至其他應用程式後再返回,使用者也預期應用程式的 UI 狀態將維持同樣的狀態。舉例來說,使用者在畫面上搜尋,接著按下「主畫面」按鈕或接聽來電,當他們再回到搜尋畫面時,會預期能夠找到先前的搜尋關鍵字與搜尋結果。
在此情況下,您的應用程式會在背景執行,系統會盡可能讓應用程式程序保持在記憶體中。不過,當使用者與其他應用程式互動而不再使用您的應用程式時,系統可能會刪除該應用程式程序。此時,系統會刪除主機活動,以及當中儲存的所有狀態。重新啟動應用程式後,畫面會以非使用者預期的乾淨狀態開始。如要進一步瞭解程序終止,請參閱「程序和應用程式生命週期」。
保留使用者介面狀態的選項
當使用者對 UI 狀態的預期與預設系統行為不符時,您必須儲存並還原使用者的 UI 狀態,以確保系統啟動的刪除作業對使用者而言是透明的。
每個保留 UI 狀態的選項在以下各方面對使用者體驗造成不同的影響:
| ViewModel | 已儲存狀態 | 永久儲存空間 | |
|---|---|---|---|
| 儲存位置 | 在記憶體中 | 在記憶體中 | 在磁碟或網路上 |
| 在設定變更後仍持續 | 是 | 是 | 是 |
| 在系統啟動的程序終止後仍持續 | 否 | 是 | 是 |
在使用者完全關閉畫面後仍持續/finish() |
否 | 否 | 是 |
| 資料限制 | 複雜物件沒有問題,但儲存空間會因可用記憶體而受限 | 僅適用於原始型別和簡易的小型物件,例如 String |
僅受限於磁碟空間,或擷取網路資源的費用 / 時間 |
| 讀取/寫入時間 | 快 (只須存取記憶體) | 慢 (須執行序列化/去序列化作業) | 慢 (須存取磁碟或執行網路交易) |
使用 ViewModel 處理設定變更
當使用者頻繁使用應用程式時,ViewModel 可以有效儲存及管理 UI 相關資料。ViewModel 能快速存取 UI 資料,在旋轉、調整視窗大小或其他常見的設定變更發生時,無須從網路或磁碟重新擷取資料。如要瞭解如何導入 ViewModel,請參閱「ViewModel 指南」。
ViewModel 會將資料保存在記憶體中,比從磁碟或網路擷取資料的費用更低。ViewModel 會與生命週期擁有者 (例如 Navigation 目的地或活動) 建立關聯。在設定變更期間,ViewModel 會保留在記憶體中,且系統會自動將 ViewModel 與因為設定變更而產生的新生命週期擁有者例項建立關聯。
與已儲存狀態不同,ViewModels 會在系統啟動程序終止時刪除。如要在 ViewModel 中,於系統啟動的程序終止事件後重新載入資料,請使用 SavedStateHandle API。或者,如果資料與 UI 相關,且不需要保留在 ViewModel 中,請使用 rememberSerializable。如果是原始資料類型,或是您不想使用 @Serializable 的情況,請使用 rememberSaveable。如果資料是應用程式資料,最好將其保存到磁碟。
如果您已經有記憶體內解決方案,可以在設定變更期間儲存 UI 狀態,可能就不需要使用 ViewModel。
使用已儲存的狀態做為備份,處理系統啟動的程序終止事件
如果系統刪除並重新建立元件,Compose 中的 rememberSerializable 和 rememberSaveable,以及 ViewModel 中的 SavedStateHandle 等 API,會儲存重新載入 UI 狀態所需的資料。為更有效率地處理複雜的資料結構,SavedStateHandle 透過 saved {} 擴充功能支援 Kotlinx 序列化,讓您能連同標準原始型別,一併無縫保存及還原型別安全物件。如要瞭解如何使用 rememberSaveable 實作已儲存狀態,請參閱「狀態和 Jetpack Compose」。
已儲存的狀態組合在設定變更或程序終止時仍會持續,但會因不同的 API 將資料序列化,而受到儲存空間和速度的限制。如果要序列化的是複雜物件,序列化作業可能會耗用大量記憶體。因為設定變更會在主執行緒上發生,因此長時間執行序列化可能會丟失影格數和視覺延遲。
請勿使用已儲存的狀態儲存大量資料 (例如點陣圖),或是需要長時間序列化或去序列化的複雜資料結構。只儲存原始類型,以及簡單的小型物件,例如 String。因此,請使用已儲存的狀態儲存最少量必要資料 (例如 ID),如果其他持續性機制失敗,就能重新建立將 UI 還原至先前狀態所需的資料。大多數應用程式都應實作這項功能,以處理系統啟動的程序終止。
視應用程式的使用案例而定,您可能根本不需要使用儲存的狀態。舉例來說,瀏覽器可能會將使用者導向他們離開瀏覽器前正在瀏覽的網頁。如果您的活動是以這種方式運作,您可以略過已儲存的狀態,改為在本機儲存所有內容。
此外,當您從意圖開啟活動時,系統會在設定變更和系統還原活動時,將額外組合程式傳送至活動中。啟動活動時,如果系統傳送一系列 UI 狀態資料 (例如搜尋查詢),做為額外意圖,您可以使用額外套件,而非已儲存狀態的軟體包。如要進一步瞭解意圖額外資訊,請參閱「意圖和意圖篩選器」。
無論是哪一種情況,您都應使用 ViewModel,避免在設定變更期間,浪費資源從資料庫重新載入資料。
如果保留 UI 資料既簡單簡易,則可單獨使用已儲存狀態 API 保留您的狀態資料。
使用 SavedStateRegistry 開啟已儲存的狀態
從 Fragment 1.1.0 或其遞移依附元件 Activity 1.0.0 開始,UI 元件 (例如 ComponentActivity) 會實作 SavedStateRegistryOwner,並提供繫結至該元件的 SavedStateRegistry。SavedStateRegistry 可讓元件擷取到儲存狀態中,以消耗或使用。舉例來說,ViewModel 的「已儲存狀態」模組會使用 SavedStateRegistry 建立 SavedStateHandle,並將其提供至 ViewModel 物件。您可以從生命週期擁有者呼叫 savedStateRegistry 來擷取 SavedStateRegistry。
影響已儲存狀態的元件必須導入 SavedStateRegistry.SavedStateProvider,這會定義名為 saveState() 的單一方法。saveState() 方法可讓元件傳回包含應從該元件儲存的任何狀態的 Bundle。SavedStateRegistry 會在生命週期擁有者生命週期的儲存狀態階段呼叫這個方法。
class SearchManager : SavedStateRegistry.SavedStateProvider {
companion object {
private const val QUERY = "query"
}
private val query: String? = null
...
override fun saveState(): Bundle {
return bundleOf(QUERY to query)
}
}
如要註冊 SavedStateProvider,請在 SavedStateRegistry 上呼叫 registerSavedStateProvider(),並傳送要與供應商資料和供應商建立關聯的金鑰。先前已儲存的供應商資料,您可以從 SavedStateRegistry 呼叫 consumeRestoredStateForKey() 查找並傳遞有金鑰關聯的供應商資料。
在 ComponentActivity 中,您可以在呼叫 super.onCreate() 後在 onCreate() 中註冊 SavedStateProvider。或者,您也可以在 SavedStateRegistryOwner 設定LifecycleObserver,用來導入 LifecycleOwner,並在 ON_CREATE 事件發生時,註冊 SavedStateProvider。使用 LifecycleObserver 時,您可以透過 SavedStateRegistryOwner 本身將先前儲存的狀態的註冊和擷取作業分離。
class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
companion object {
private const val PROVIDER = "search_manager"
private const val QUERY = "query"
}
private val query: String? = null
init {
// Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
val registry = registryOwner.savedStateRegistry
// Register this object for future calls to saveState()
registry.registerSavedStateProvider(PROVIDER, this)
// Get the previously saved state and restore it
val state = registry.consumeRestoredStateForKey(PROVIDER)
// Apply the previously saved state
query = state?.getString(QUERY)
}
}
}
override fun saveState(): Bundle {
return bundleOf(QUERY to query)
}
...
}
class SearchActivity : ComponentActivity() {
private var searchManager = SearchManager(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set up your Compose UI here
setContent {
// ...
}
}
}
使用本機常駐性來處理複雜或大型資料的程序終止
只要應用程式安裝在使用者的裝置上,常駐本機儲存空間 (例如資料庫或 DataStore) 會持續有效 (除非使用者清除應用程式資料)。雖然這類本機儲存空間在由系統啟動應用程式程序終止時仍然有效,但擷取成本較高,因為必須從本機儲存空間讀取至記憶體。永久的本機儲存空間通常可能是應用程式架構的一部分,用於儲存您不想在開啟和關閉應用程式時失去的所有資料。
ViewModel 和使用 rememberSerializable、rememberSaveable 或 SavedStateHandle 儲存的狀態都不是長期儲存解決方案,因此不能取代本機儲存空間 (例如資料庫)。請改用這些機制來暫時儲存過渡 UI 狀態,並將永久儲存空間用於其他應用程式資料。請參閱「應用程式架構指南」,進一步瞭解如何運用本機儲存空間長期保留應用程式模型資料 (例如裝置重新啟動時)。
管理使用者介面狀態:分割及管理
將工作分配給各種不同的持續性機制,即可有效率地儲存和還原 UI 狀態。在大部分情況下,上述機制都應根據資料複雜性、存取速度和生命週期的取捨,儲存應用程式所用的不同資料類型:
- 本機持續性:儲存開啟和關閉應用程式時您不想失去的所有應用程式資料。
- 例如:一組精選歌曲物件,其中可包含音訊檔案和中繼資料。
ViewModel:將顯示相關聯 UI (即畫面 UI 狀態) 所需的所有資料儲存在記憶體中。- 範例:最近查詢和最近搜尋查詢的歌曲物件。
- 儲存的狀態 (
rememberSerializable、rememberSaveable和SavedStateHandle):儲存少量資料。如果系統停止作業,系統會重新建立 UI,然後重新載入該 UI 的狀態。請將複雜物件儲存在本機儲存空間中,並將物件的專屬 ID 儲存在已儲存狀態 API,而非在這裡儲存複雜物件。- 範例:儲存最近的搜尋查詢。
舉例來說,可以讓您在歌曲庫搜尋的應用程式。各種事件的處理方式如下:
使用者新增歌曲時,ViewModel 會立即委派將這項資料保留在本機。如果這個新加入的歌曲應該顯示在使用者介面中,您也應該更新 ViewModel 物件中的資料來反映新增的歌曲。請記得將主執行緒插入所有資料庫。
當使用者搜尋歌曲時,從資料庫載入的任何複雜歌曲資料,會立即儲存在 ViewModel 物件中,做為畫面 UI 狀態的一部分。
當應用程式進入背景且系統儲存狀態時,應使用已儲存狀態 API 儲存搜尋查詢,以防程序重新建立。由於載入儲存在其中的應用程式資料時需要這項資訊,因此請將搜尋查詢儲存在 ViewModel SavedStateHandle 中,或在可組合函式中使用 rememberSerializable 或 rememberSaveable。這是載入資料並將 UI 還原至目前狀態所需的所有資訊。
還原複雜狀態:重新組合元素
使用者返回應用程式時,可能會有兩種重新建立 UI 的情況:
- 系統終止應用程式程序後,要重新建立 UI。系統已使用儲存狀態 API 儲存查詢。
ViewModel(使用SavedStateHandle) 或可組合函式 (使用rememberSerializable或rememberSaveable) 會自動還原查詢。如果可組合函式還原查詢,就會將查詢傳遞至ViewModel。ViewModel發現沒有快取任何搜尋結果時,將使用特定搜尋查詢載入搜尋結果。 - 設定變更後,系統會重新建立 UI。由於
ViewModel執行個體尚未遭到破壞,因此ViewModel會將所有資訊快取在記憶體中,不需要重新查詢資料庫。
其他資源
如要進一步瞭解如何儲存 UI 狀態,請參閱下列資源。
程式碼研究室
Views content
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- ViewModel 的已儲存狀態模組
- 使用生命週期感知元件處理生命週期
- ViewModel 總覽