您可以根據狀態提升至的位置和需要的邏輯,使用不同的 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) } }
showDetails
是一個布林值變數,作用是記錄即時通訊泡泡處於收合或展開狀態。
rememberSaveable
會透過儲存的例項狀態機制,將 UI 元素狀態儲存在 Bundle
中。
基本類型可以自動儲存在 bundle 中。如果狀態保存在非原始的類型 (例如資料類別) 中,您可以使用不同的儲存機制,例如使用 Parcelize
註解、使用 listSaver
和 mapSaver
等 Compose API,或是實作擴充 Compose 執行階段 Saver
類別的自訂儲存工具類別。如要進一步瞭解這些方法,請參閱「儲存狀態的方式」說明文件。
在以下程式碼片段中,rememberLazyListState
Compose API 會使用 rememberSaveable
儲存 LazyListState
,其中包含 LazyColumn
或 LazyRow
的捲動狀態。該 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
只要使用 SavedStateHandle
的 saveable
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
並收集最新值。
在以下程式碼片段中,savedFilterType
是 StateFlow
變數,用於儲存套用至即時通訊應用程式清單管道清單的篩選器類型:
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 (rememberSaveable
和 SavedStateHandle
) 來儲存少量 UI 狀態。這些資料是搭配其他儲存機制時,將 UI 還原至先前狀態所需要的最低限度資料。舉例來說,如果您將使用者先前查看的設定檔 ID 儲存在套件中,就能從資料層擷取設定檔詳細資料等大量資料。
如要進一步瞭解各種儲存 UI 狀態的方式,請參閱「儲存 UI 狀態」說明文件和架構指南的「資料層」頁面。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 在何種情況下提升狀態
- 狀態和 Jetpack Compose
- 清單和格線