在 Compose 中儲存 UI 狀態

您可以根據狀態提升至的位置和需要的邏輯,使用不同的 API 來儲存及還原 UI 狀態。每個應用程式都會利用一組 API 妥善達成此目的。

任何 Android 應用程式都可能會因為重新建立活動或程序而失去 UI 狀態。以下是可能引發這類情況的事件:

  • 設定變更。除非是使用者手動變更設定,否則系統會先刪除活動,再重新建立活動。
  • 系統終止程序。當應用程式在背景執行時,裝置會釋出記憶體等資源,供其他程序使用。

應用程式是否在發生這些事件後保留狀態,對於提供良好的使用者體驗至關重要,而您該選擇保留哪些狀態,則視應用程式獨特的使用者流程而定。最佳做法是至少保存使用者輸入內容和瀏覽相關狀態,比如清單捲動位置、使用者想查看詳細資料的項目 ID、正在選擇的偏好設定,或是在文字欄位中輸入的內容。

本頁歸納了所有可以儲存 UI 狀態的 API,請根據狀態提升至的位置和需要的邏輯,選擇合適的 API。

UI 邏輯

如果狀態是在 UI 中提升,那麼只要在可組合函式或範圍限定為組合的純狀態容器類別中使用 rememberSaveable,即可在重新建立活動和程序後保留狀態。

在以下程式碼片段中,rememberSaveable 的作用是儲存單一 UI 元素元素狀態:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

圖 1. 依輕觸操作展開與收合的即時通訊訊息泡泡。

showDetails 是一個布林值變數,作用是記錄即時通訊泡泡處於收合或展開狀態。

rememberSaveable 會透過儲存的例項狀態機制,將 UI 元素狀態儲存在 Bundle 中。

基本類型可以自動儲存在 bundle 中。如果狀態保存在非原始的類型 (例如資料類別) 中,您可以使用不同的儲存機制,例如使用 Parcelize 註解、使用 listSavermapSaver 等 Compose API,或是實作擴充 Compose 執行階段 Saver 類別的自訂儲存工具類別。如要進一步瞭解這些方法,請參閱「儲存狀態的方式」說明文件。

在以下程式碼片段中,rememberLazyListState Compose API 會使用 rememberSaveable 儲存 LazyListState,其中包含 LazyColumnLazyRow 的捲動狀態。該 API 使用的 LazyListState.Saver 是可以儲存及還原捲動狀態的自訂 Saver,捲動狀態會在活動或程序重新建立後 (例如在變更裝置螢幕方向等設定變更) 後保留捲動狀態。

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

最佳做法

rememberSaveable 使用 Bundle 儲存 UI 狀態,此狀態將與其他同樣會寫入資料的 API 分享,比如活動中的 onSaveInstanceState() 呼叫。不過,這個 Bundle 的大小有限,儲存大型物件可能導致執行階段中發生 TransactionTooLarge 例外狀況。如果在單一 Activity 應用程式中使用相同的 Bundle,這就可能特別發生問題。

為了避免這種異常終止情形,請「切勿在 bundle 中儲存複雜的物件或物件清單」

您應該只儲存最低限度需要的狀態資料,例如 ID 或鍵,並使用這些資料將還原更複雜 UI 狀態的工作交由其他機制 (例如永久儲存空間) 處理。

請根據應用程式的用途,選擇能滿足使用者需求的設計。

驗證狀態還原

您可以驗證使用 rememberSaveable 儲存在 Compose 元素中的狀態是否會在活動或程序重新建立後正確還原。有些 API 就是為此設計,比如 StateRestorationTester。詳情請參閱測試說明文件。

商業邏輯

如果 UI 元素狀態因為商業邏輯而需提升至 ViewModel,您可以使用 ViewModel 的 API。

在 Android 應用程式中使用 ViewModel 的主要優點之一,就是能不耗資源地處理設定變更。當系統因為設定變更而刪除又重新建立活動時,提升至 ViewModel 的 UI 狀態會保留在記憶體中。重新建立完畢後,舊的 ViewModel 例項會附加到新的活動例項中。

不過,ViewModel 例項無法在系統終止程序後繼續存留。如果想保留 UI 狀態,請使用 ViewModel 的儲存狀態模組,其中含有 SavedStateHandle API。

最佳做法

SavedStateHandle 也會使用 Bundle 機制來儲存 UI 狀態,因此只適合用來儲存簡單的 UI 元素狀態

畫面 UI 狀態是藉由應用商業規則及存取應用程式 UI 之外的層所產生,由於其潛在複雜度和大小,所以不應儲存在 SavedStateHandle 中。您可以利用其他機制來儲存複雜或龐大的資料,例如 本機永久儲存空間。重新建立程序後,系統會使用儲存在 SavedStateHandle 中的暫時性狀態 (如有) 還原畫面,並再次從資料層產生畫面 UI 狀態。

SavedStateHandle API

SavedStateHandle 有各種用來儲存 UI 元素狀態的 API,最值得注意的是:

撰寫 State saveable()
StateFlow getStateFlow()

撰寫 State

只要使用 SavedStateHandlesaveable API,以 MutableState 的形式讀取及寫入 UI 元素狀態,即可用最低限度的程式碼設定,在活動和程序重新建立後保留狀態。

saveable API 預設支援基本類型,可接收 stateSaver 參數以使用自訂 Saver,就和 rememberSaveable() 一樣。

在以下程式碼片段中,message 會將使用者輸入類型儲存至 TextField

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

如要進一步瞭解如何使用 saveable API,請參閱 SavedStateHandle 說明文件。

StateFlow

使用 getStateFlow() 儲存 UI 元素狀態,將其做為來自 SavedStateHandle 的資料流使用。StateFlow 具唯讀性質,需要您指定鍵,才能替換資料流來發送新值。設定好的金鑰後,就能擷取 StateFlow 並收集最新值。

在以下程式碼片段中,savedFilterTypeStateFlow 變數,用於儲存套用至即時通訊應用程式清單管道清單的篩選器類型:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

每當使用者選取新的篩選器類型,就會呼叫 setFiltering。這會透過 _CHANNEL_FILTER_SAVED_STATE_KEY_ 鍵在 SavedStateHandle 中儲存新值。savedFilterType 是會發送該鍵所儲存最新值的資料流。filteredChannels 會訂閱資料流,以執行頻道篩選。

如要進一步瞭解 getStateFlow() API,請參閱 SavedStateHandle 說明文件。

摘要

下表歸納了本節介紹的 API,以及何時該使用這些 API 儲存 UI 狀態:

活動 UI 邏輯 ViewModel 中的商業邏輯
設定變更 rememberSaveable 自動
系統終止程序 rememberSaveable SavedStateHandle

要使用哪一個 API,取決於狀態儲存的位置和需要狀態的邏輯。對於 UI 邏輯中使用的狀態,請使用 rememberSaveable;對於商業邏輯中使用的狀態,如果狀態保存在 ViewModel 中,請使用 SavedStateHandle 進行儲存。

建議您使用 bundle API (rememberSaveableSavedStateHandle) 來儲存少量 UI 狀態。這些資料是搭配其他儲存機制時,將 UI 還原至先前狀態所需要的最低限度資料。舉例來說,如果您將使用者先前查看的設定檔 ID 儲存在套件中,就能從資料層擷取設定檔詳細資料等大量資料。

如要進一步瞭解各種儲存 UI 狀態的方式,請參閱「儲存 UI 狀態」說明文件和架構指南的「資料層」頁面。