保存界面状态
在 Jetpack Compose 应用中,正确保存和恢复界面状态对于提供良好的用户体验至关重要。当应用因重新创建 activity 或进程而丢失界面状态时,用户可能会遇到数据丢失或界面不一致的问题。本指南将详细介绍如何在 Jetpack Compose 中保存和恢复界面状态。
1. 界面状态丢失的原因
Android 应用可能会因以下事件而丢失界面状态:
- 系统发起的进程终止:如内存不足时系统杀掉进程。
- 配置更改:如屏幕旋转、语言切换等。
- 用户发起的进程终止:如用户显式关闭应用。
对于用户发起的进程终止,瞬时状态的丢失通常是合理的。但在其他情况下,保留状态对于提供流畅的用户体验至关重要。
2. 保存界面状态的策略
根据状态提升的位置和所需的逻辑,可以使用不同的 API 来保存和恢复界面状态。
2.1 界面逻辑中的状态保存
当状态在界面逻辑中提升时(无论是在可组合函数还是作用域限定为组合的普通状态容器类中),可以使用 rememberSaveable 在重新创建 activity 和进程之后保留状态。
示例:保存单个布尔值界面元素状态
kotlin
@Composable
fun ChatBubble(message: Message) {
var showDetails by rememberSaveable { mutableStateOf(false) }
ClickableText(
text = AnnotatedString(message.content),
onClick = { showDetails = !showDetails }
)
if (showDetails) {
Text(message.timestamp)
}
}
存储机制:
rememberSaveable通过保存的实例状态机制将界面元素状态存储在 Bundle 中。- 它能够自动将基元类型存储到 Bundle 中。对于非基元类型(如数据类),可以使用
Parcelize注解、listSaver和mapSaver等 Compose API,或实现自定义的Saver类。
示例 :保存 LazyListState
kotlin
@Composable
fun rememberLazyListState(
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
return rememberSaveable(saver = LazyListState.Saver) {
LazyListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset)
}
}
2.2 业务逻辑中的状态保存
当界面元素状态因业务逻辑需要被提升到 ViewModel 时,可以使用 ViewModel 的 API 来保存状态。
ViewModel 的优势:
- ViewModel 实例在配置更改(如屏幕旋转)后仍然存在,因此提升到 ViewModel 的界面状态会保留在内存中。
- 但 ViewModel 实例在系统发起的进程终止后将失效。为了持久化状态,可以使用
SavedStateHandle。
示例 :使用 SavedStateHandle 保存界面元素状态
kotlin
class ConversationViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(""))
}
private set
fun update(newMessage: TextFieldValue) {
message = newMessage
}
}
@Composable
fun UserInput(viewModel: ConversationViewModel = viewModel()) {
TextField(
value = viewModel.message,
onValueChange = { viewModel.update(it) }
)
}
SavedStateHandle 的 API:
saveableAPI :以MutableState形式读取和写入界面元素状态,支持基元类型和自定义Saver。getStateFlowAPI :存储界面元素状态,并将其用作来自SavedStateHandle的数据流。
3. 最佳实践
3.1 界面逻辑中的最佳实践
- 避免存储大型对象 :
rememberSaveable使用 Bundle 存储界面状态,Bundle 大小有限。存储大型复杂对象或对象列表可能会导致TransactionTooLargeException。 - 存储最少必需状态:仅存储恢复界面状态所需的最少数据(如 ID 或键),并使用这些数据从数据层重新生成复杂状态。
3.2 业务逻辑中的最佳实践
- 使用
SavedStateHandle存储简单状态 :SavedStateHandle也使用 Bundle 机制,因此应仅用于存储简单的界面元素状态。 - 复杂状态使用永久性存储 :屏幕界面状态可能非常复杂且庞大,不应存储在
SavedStateHandle中。可以使用本地永久性存储(如 Room 数据库)来存储复杂数据。
4. 验证状态恢复
为了验证重新创建 activity 或进程后,Compose 元素中使用 rememberSaveable 存储的状态是否已正确恢复,可以使用 StateRestorationTester。
示例 :使用 StateRestorationTester 验证状态恢复
kotlin
@Test
fun testStateRestoration() {
val tester = StateRestorationTester(context)
tester.setContent {
val (value, setValue) = rememberSaveable { mutableStateOf(0) }
// Test logic here
}
}
5. 总结
- 界面逻辑中的状态 :使用
rememberSaveable在重新创建 activity 和进程后保留状态。 - 业务逻辑中的状态 :如果状态被提升到 ViewModel,使用
SavedStateHandle保存状态。 - 最佳实践:避免在 Bundle 中存储大型复杂对象,存储最少必需状态,并使用永久性存储处理复杂数据。
通过合理使用这些 API 和最佳实践,可以确保 Jetpack Compose 应用在面对配置更改和进程终止时保持一致的用户体验。