儲存 UI 狀態

本指南說明使用者對 UI 狀態的期望,以及可用的保留狀態選項。

在系統刪除活動或應用程式後,快速儲存及還原活動的 UI 狀態,對於提供良好的使用者體驗至關重要。使用者預期 UI 狀態保持不變,但系統可能會刪除活動及其儲存狀態。

如要彌補使用者預期和系統行為之間的差距,請組合下列方法:

最佳解決方案取決於 UI 資料的複雜度、應用程式的用途,並在資料存取速度和記憶體用量之間取得平衡。

請確保您的應用程式符合使用者預期,並提供快速回應的介面。避免在將資料載入 UI 時發生延遲情形,尤其是在常見的設定變更 (例如旋轉) 之後。

使用者期望和系統行為

視使用者採取的行動而定,使用者會希望能夠清除或保留活動狀態。在某些情況下,系統會自動執行使用者預期的操作。在其他情況下,系統會採用與使用者預期的相反情況。

使用者啟動的使用者介面狀態關閉

使用者預期在啟動活動時,該活動的暫時 UI 狀態會保持不變,直到使用者完全關閉活動為止。使用者可以按照下列步驟完全關閉活動:

  • 將活動從「總覽」(最近) 畫面滑出。
  • 在「設定」畫面中終止或強制退出應用程式。
  • 正在重新啟動裝置。
  • 完成某種「完成」動作 (由 Activity.finish() 支援)。

使用者在這些完整關閉情況下的假設,是他們已永久離開活動,如果重新開啟活動,活動則會從乾淨狀態開始。這些關閉情境的基礎系統行為與使用者的預期相符:活動例項會刪除,並從記憶體中移除,以及儲存在其中的任何狀態和所有與該活動相關的例項狀態記錄。

這項關於完全關閉的規則有部分例外情況,例如,使用者可能會預期瀏覽器會在他們使用返回按鈕離開瀏覽器前,將他們導向當時正在瀏覽的網頁。

系統啟動的使用者介面狀態關閉

使用者會預期在設定變更 (例如旋轉或切換至多視窗模式) 期間,活動的 UI 狀態都會保持不變。不過,當這類設定變更發生時,系統會刪除活動,並清除儲存在活動執行個體中的任何 UI 狀態。如要進一步瞭解裝置設定,請參閱「設定參考資料」頁面。請注意,您可以覆寫設定變更的預設行為,但我們不建議這麼做。詳情請參閱「自行處理設定變更」。

如果暫時切換至其他應用程式,稍後再返回應用程式,使用者也會希望活動的 UI 狀態保持不變。舉例來說,使用者在您的搜尋活動中執行搜尋,接著按下主畫面按鈕或接聽電話時,他們會想回到搜尋活動時,可以像之前一樣找到搜尋關鍵字及其搜尋結果。

在此情況下,您的應用程式會放在背景中,系統會盡可能將應用程式程序保留在記憶體中。不過,當使用者與其他應用程式互動時,系統可能會刪除應用程式程序。在這種情況下,系統會刪除活動例項,以及當中儲存的所有狀態。使用者重新啟動應用程式時,活動會意外處於乾淨狀態。如要進一步瞭解程序終止,請參閱「程序和應用程式生命週期」。

保留使用者介面狀態的選項

如果使用者對 UI 狀態的期望與預設系統行為不符,您必須儲存並還原使用者的 UI 狀態,確保系統啟動的刪除作業對使用者透明化。

每個保留 UI 狀態的選項,會因下列影響使用者體驗的維度而有所不同:

ViewModel 已儲存執行個體狀態 永久儲存空間
儲存位置 在記憶體中 在記憶體中 在磁碟或網路上
在設定變更後仍然有效
在系統終止程序後仍持續有效
在使用者完成活動關閉後仍持續/onFinish()
資料限制 使用複雜的物件,但空間受限於可用記憶體 僅適用於原始類型和簡易的小型物件,例如字串 僅受限於磁碟空間,或從網路資源擷取的費用 / 時間
讀取/寫入時間 快速 (僅限記憶體存取) 緩慢 (須進行序列化/去序列化) 慢 (須存取磁碟或網路交易)

使用 ViewModel 處理設定變更

當使用者積極使用應用程式時,ViewModel 可以有效儲存及管理 UI 相關資料。這個程式庫可讓您快速存取 UI 資料,避免在旋轉、調整視窗大小和其他常見的設定變更時,從網路或磁碟重新擷取資料。如要瞭解如何實作 ViewModel,請參閱 ViewModel 指南

ViewModel 會將資料保留在記憶體中,這表示從磁碟或網路擷取資料的費用較為低廉。ViewModel 會與活動 (或其他生命週期擁有者) 建立關聯 - ViewModel 會在設定變更期間保留在記憶體中,且系統會自動將 ViewModel 與因設定變更而產生的新活動例項建立關聯。

當使用者退出活動或片段,或者您呼叫 finish() 時,系統會一併刪除 ViewModel,系統會在使用者預期這些情況下清除狀態。

與已儲存的例項狀態不同,ViewModels 會在系統啟動程序終止時刪除。如要在 ViewModel 中由系統啟動程序終止後重新載入資料,請使用 SavedStateHandle API。或者,如果資料與 UI 相關,且不需要保存在 ViewModel 中,請在 View 系統中使用 onSaveInstanceState(),或在 Jetpack Compose 中使用 rememberSaveable。如果資料是「應用程式資料」,則最好將資料保存至磁碟。

如果您已經有記憶體內解決方案,可在設定變更期間儲存 UI 狀態,可能就不需要使用 ViewModel。

使用已儲存的例項狀態做為備用,處理系統啟動的程序終止

檢視畫面系統中的 onSaveInstanceState() 回呼、Jetpack Compose 中的 rememberSaveable 和 ViewModel 中的 SavedStateHandle,會儲存重新載入 UI 控制器狀態 (例如活動或片段) 所需的資料 (如果系統刪除後又重新建立該控制器)。如要瞭解如何使用 onSaveInstanceState 實作已儲存的例項狀態,請參閱「活動生命週期指南」中的「儲存及還原活動狀態」。

已儲存的例項狀態組合在設定變更和程序終止期間都會持續存在,但由於不同的 API 會將資料序列化,因此會受到儲存空間和速度的限制。如果序列化的物件是複雜,序列化可能會耗用大量記憶體。由於這項設定會在設定變更期間於主要執行緒上進行,因此長時間執行序列化作業可能會導致捨棄影格和視覺延遲。

請勿使用已儲存的例項狀態來儲存大量資料 (例如點陣圖),或是需要長時間序列化或去序列化的複雜資料結構。請改為只儲存原始類型和簡單的小型物件,例如 String。因此,當其他持續性機制失敗時,請使用已儲存的例項狀態儲存最低限度的資料 (例如 ID),重新建立將 UI 還原至先前狀態所需的資料。大多數應用程式都應實作這個項目,以處理系統啟動的程序終止。

視應用程式的用途而定,您可能完全不需要使用已儲存的執行個體狀態。舉例來說,瀏覽器可能會將使用者帶往他們在離開瀏覽器前正在瀏覽的確切網頁。如果活動以這種方式運作,您可以使用已儲存的例項狀態來撤銷,改為在本機保留所有內容。

此外,當您從意圖開啟活動時,系統會在設定變更和系統還原活動時,將額外項目套件傳送至活動。如果啟動活動時,系統會傳遞一段 UI 狀態資料 (例如搜尋查詢) 做為額外意圖,您可以使用額外套件,而非已儲存的例項狀態組合。如要進一步瞭解意圖額外項目,請參閱「意圖和意圖篩選器」。

不論是哪一種情況,您都應使用 ViewModel,以避免在設定變更期間,浪費週期從資料庫重新載入資料。

如果保留 UI 資料既簡單又輕量,您可以單獨使用已儲存的執行個體狀態 API 來保留狀態資料。

使用 SavedStateRegistry 開啟已儲存的狀態

Fragment 1.1.0 或其遞移依附元件 Activity 1.0.0 開始,UI 控制器 (例如 ActivityFragment) 開始實作 SavedStateRegistryOwner,並提供繫結至該控制器的 SavedStateRegistrySavedStateRegistry 可讓元件掛入 UI 控制器的儲存狀態,以使用或提供其內容。舉例來說,ViewModel 的已儲存狀態模組會使用 SavedStateRegistry 建立 SavedStateHandle,並提供給 ViewModel 物件。只要呼叫 getSavedStateRegistry(),即可從 UI 控制器中擷取 SavedStateRegistry

影響已儲存狀態的元件必須實作 SavedStateRegistry.SavedStateProvider,定義名為 saveState() 的單一方法。saveState() 方法可讓元件傳回 Bundle,其中包含應從該元件儲存的任何狀態。SavedStateRegistry 會在 UI 控制器生命週期的儲存狀態階段呼叫這個方法。

Kotlin

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)
    }
}

Java

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(),並傳入供應器資料的關聯鍵,就可以從儲存的狀態擷取先前儲存的資料。

ActivityFragment 中,呼叫 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);
    ...
}

使用本機常駐性來處理複雜或大型資料的程序終止

只要應用程式安裝在使用者的裝置上,常駐本機儲存空間 (例如資料庫或共用偏好設定) 仍然有效 (除非使用者清除應用程式的資料)。雖然這類本機儲存空間在系統啟動的活動和應用程式程序終止時仍然有效,但擷取成本可能相當高昂,因為這項作業必須從本機儲存空間讀取至記憶體。此永久性本機儲存空間通常可能已構成應用程式架構的一部分,用於儲存您不想在開啟及關閉活動時遺失的所有資料。

不論是 ViewModel 或已儲存的例項狀態,都不是長期儲存解決方案,因此不能取代本機儲存空間 (例如資料庫)。建議您改用這些機制來暫時儲存暫時 UI 狀態,並將永久儲存空間用於其他應用程式資料。請參閱應用程式架構指南,進一步瞭解如何利用本機儲存空間長期保留您的應用程式模型資料 (例如在裝置重新啟動時)。

管理使用者介面狀態:分割及管理

將工作劃分到各種類型的持續性機制中,即可有效率地儲存及還原 UI 狀態。在多數情況下,根據資料複雜度、存取速度和生命週期的取捨,這些機制都應儲存活動中使用的不同類型資料:

  • 本機永久性:儲存所有不想遺失的應用程式資料 (當您開啟及關閉活動時)。
    • 範例:歌曲物件集合,可包含音訊檔案和中繼資料。
  • ViewModel:將顯示相關 UI 所需的所有資料 (即畫面 UI 狀態) 儲存在記憶體中。
    • 例如:最近搜尋的歌曲物件和最近搜尋查詢。
  • 已儲存例項狀態:儲存系統停止並重新建立 UI 時,重新載入 UI 狀態所需的少量資料。請將複雜的物件保存在本機儲存空間,並將這些物件的專屬 ID 儲存在已儲存的執行個體狀態 API 中,而不要在這裡儲存複雜的物件。
    • 範例:儲存最近的搜尋查詢。

例如可讓您在歌曲資料庫中搜尋的活動。各種事件的處理方式如下:

使用者新增歌曲時,ViewModel 會立即委派將這項資料保留在本機。如果這個新加入的歌曲應該顯示在 UI 中,您也應該更新 ViewModel 物件中的資料,以反映新增的歌曲。請記得將所有資料庫插入主執行緒外。

使用者搜尋歌曲時,無論從資料庫載入的任何複雜歌曲資料,都應該立即儲存在 ViewModel 物件中,做為螢幕 UI 狀態的一部分。

當活動進入背景,且系統呼叫已儲存的執行個體狀態 API 時,搜尋查詢應儲存在已儲存的執行個體狀態中,以便在重新建立程序時使用。由於必須取得資訊才能載入保存的應用程式資料,因此請將搜尋查詢儲存在 ViewModel SavedStateHandle 中。這些是載入資料並讓 UI 恢復目前狀態所需的所有資訊。

還原複雜狀態:重組片段

使用者恢復活動時,有兩種可能的情況:重新建立活動:

  • 系統停止活動後,會重新建立活動。系統會將查詢儲存在已儲存的執行個體狀態套件中,如未使用 SavedStateHandle,UI 應將查詢傳送至 ViewModelViewModel 會發現沒有快取任何搜尋結果,並委派使用特定搜尋查詢載入搜尋結果。
  • 活動會在設定變更後建立。由於 ViewModel 執行個體尚未刪除,ViewModel 已在記憶體中快取所有資訊,因此不需要重新查詢資料庫。

其他資源

如要進一步瞭解如何儲存 UI 狀態,請參閱下列資源。

網誌