compose 状态恢复笔记

7 阅读3分钟

Compose 中的 状态恢复 指的是在 配置变更(如屏幕旋转、语言切换)或 进程终止后,将 UI 状态和数据恢复到之前状态的能力。这是保障用户体验的关键。

它主要解决两个核心场景:

  1. 配置变更:Activity 被销毁重建,但进程存活。
  2. 进程终止:App 被系统从后台杀死后重新打开。
flowchart TD
    A[“状态恢复需求”] --> B{“首先判断状态类型”}
    
    B --> C[“UI临时状态<br>(如输入框文字、滚动位置)”]
    B --> D[“屏幕/业务逻辑状态<br>(如用户列表、页面选中项)”]
    B --> E[“永久/用户偏好状态<br>(如主题、登录令牌)”]
    
    C --> F{“该状态是否重要到<br>需要跨配置变更保留?”}
    F -->|否,可丢弃| G[“使用 remember”]
    F -->|是,需保留| H[“使用 rememberSaveable”]
    
    D --> I[“首选方案:ViewModel”]
    I --> I1[“配合 SavedStateHandle<br>处理基本数据类型”]
    I --> I2[“配合 Hilt 等依赖注入框架<br>注入复杂依赖”]
    
    E --> J[“持久化方案<br>DataStore / Room / SharedPreferences”]

下面我们具体看看每种工具的原理和用法。

🧩 核心工具详解

1. rememberSaveable:UI 临时状态的救星

适用于保存简单的 UI 状态,是 remember 的增强版。其核心原理是将数据通过 Bundle 系统进行序列化/反序列化。

使用方式

@Composable
fun LoginInput() {
    // username 在屏幕旋转后会被保留
    var username by rememberSaveable { mutableStateOf("") }
    
    OutlinedTextField(
        value = username,
        onValueChange = { username = it },
        label = { Text("用户名") }
    )
}

处理复杂数据:对于无法自动保存的数据类,需提供 Saver

data class UserSettings(val theme: String, val fontSize: Int)

@Composable
fun ComplexStateDemo() {

    //mapSaver用法
    var userMapSaver = run {
        val phone = "phone"
        val password = "pasword"
        mapSaver( save = { mapOf(phone to it.phone,password to it.password) },
        restore = { UserSettings(it[phone].toString() it[password].toString()) }) 
    }
    
    //ListSaver用法
    val settingsSaver = listSaver<UserSettings, Any>(
        save = { listOf(it.theme, it.fontSize) },
        restore = { UserSettings(it[0] as String, it[1] as Int) }
    )
    
    var settings by rememberSaveable(stateSaver = settingsSaver) {
        mutableStateOf(UserSettings("Dark", 16))
    }
    // ... 使用 settings
}

注意rememberSaveable 适合轻量、与 UI 紧密相关的状态。对于重要业务状态,应使用更强大的 ViewModel

2. ViewModel:业务状态的恢复基石

ViewModel 的生命周期 长于 Activity/Fragment,因此在配置变更时不会销毁,这是其天然的状态恢复优势

对于需要在进程终止后也恢复的状态,ViewModel 需配合 SavedStateHandle 使用。

使用方式

// 1. 在 ViewModel 中接收 SavedStateHandle
class MyViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    // 2. 从 handle 中读取保存的状态,并提供默认值
    private val _selectedTab = savedStateHandle.getStateFlow("selectedTab", 0)
    val selectedTab: StateFlow<Int> = _selectedTab
    
    fun selectTab(index: Int) {
        // 3. 更新状态的同时,也保存到 handle 中
        savedStateHandle["selectedTab"] = index
        // 也可以 _selectedTab.value = index,如果使用 MutableStateFlow
    }
    
    // 对于复杂对象,可以序列化为 JSON 字符串存储
    private val _userData = savedStateHandle.getStateFlow("userData", "")
    val userData: StateFlow<User?> = _userData.map { json ->
        if (json.isEmpty()) null else Gson().fromJson(json, User::class.java)
    }.stateIn(viewModelScope, SharingStarted.Eagerly, null)
    
    fun saveUser(user: User) {
        savedStateHandle["userData"] = Gson().toJson(user)
    }
}

在可组合函数中消费:

@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val selectedTab by viewModel.selectedTab.collectAsStateWithLifecycle()
    
    TabRow(selectedTabIndex = selectedTab) {
        tabs.forEachIndexed { index, title ->
            Tab(
                selected = selectedTab == index,
                onClick = { viewModel.selectTab(index) }
            ) {
                Text(title)
            }
        }
    }
}

3. 持久化存储:应对进程终止

当应用被系统从后台彻底杀死时,ViewModelSavedStateHandle 也会被清除。此时需要真正的持久化存储

  • DataStore (推荐):现代异步 API,适用于键值对数据(如用户偏好设置)。
  • Room:功能强大的 SQLite 抽象层,适用于复杂结构化数据。
  • 文件存储:适用于大文件或特定格式数据。

在应用启动或进入相关界面时,从持久化存储中读取数据并初始化状态。

✅ 最佳实践总结

场景推荐工具关键原因
简单的 UI 状态恢复(如输入文本、开关状态)rememberSaveable使用简单,自动集成到 Bundle 系统。
屏幕级、业务逻辑状态恢复(如选中的标签页、列表项)ViewModel + SavedStateHandle生命周期更长,与 UI 逻辑分离,是 Android 架构组件推荐的标准模式。
跨进程终止恢复的用户偏好/重要数据ViewModel + 持久化存储(DataStore/Room)SavedStateHandle 仅在正常生命周期内有效,进程终止后需持久化。

通用原则

  1. 状态提升:将需要恢复的状态提升到足够高的层级(如屏幕根组件),以确保恢复逻辑能够覆盖。
  2. 单一可信源:确保每个状态只有一个真实数据源。ViewModel 通常是这个角色的最佳担当。
  3. 测试:务必测试状态恢复流程,尤其是模拟进程被终止后恢复的场景。

记住一个简单的决策链:先问这个状态是否属于业务逻辑(是 → 用 ViewModel),再问是否需要持久化(是 → 加 DataStore/Room)。如果只是临时的UI状态且值得保留,就用 rememberSaveable。 如果你能分享具体的状态恢复场景(例如什么样的数据、在什么情况下丢失),我可以提供更针对性的代码方案。