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