ViewModel 的已保存状态模块 Android Jetpack 的一部分。
保存界面状态这篇文章提到过,ViewModel 对象可以处理配置更改,因此您无需担心旋转时或其他情况下的状态。但是,如果需要处理系统发起的进程终止,则可以使用 SavedStateHandle API 作为备用方式。
界面状态通常在 ViewModel 对象中存储或引用,因此,在 Compose 中使用 rememberSaveable 时需要已保存的状态模块可以为您处理的某个样板文件。
使用此模块时,ViewModel 对象会通过其构造函数接收 SavedStateHandle 对象。此对象是一个键值对映射,用于向已保存状态写入对象以及从其中检索对象。这些值会在进程被系统终止后继续保留,并通过同一对象保持可用状态。
保存的状态与您的任务堆栈相关联。如果任务堆栈消失,保存的状态也会消失。强制停止应用、从“最近用过”菜单中移除应用或重新启动设备时,可能会发生这种情况。在这种情况下,任务堆栈会消失,并且您无法恢复保存的状态中的信息。在用户发起的界面状态解除情景中,不会恢复保存的状态。在系统发起的界面状态解除情景中,则会恢复。
设置
如需使用 SavedStateHandle,请将其作为构造函数实参传递给 ViewModel。
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
然后,您可以在可组合项中检索 ViewModel 的实例,而无需任何其他配置。默认 ViewModel 工厂会向您的 ViewModel 提供相应的 SavedStateHandle。
class MyViewModel : ViewModel() { /*...*/ } // import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
提供自定义 ViewModelProvider.Factory 实例时,您可以使用 CreationExtras 和 viewModelFactory DSL 启用 SavedStateHandle。
使用 SavedStateHandle
SavedStateHandle 类是一个键值对映射,用于通过 set() 和 get() 方法向已保存的状态写入数据以及从中检索数据。
通过使用 SavedStateHandle,查询值会在进程终止后保留下来,从而确保用户在重新创建前后看到同一组过滤后的数据,而无需 activity 或 fragment 手动保存、恢复该值并将其重新转给 ViewModel。
此外,SavedStateHandle 还包含其他您与键值对映射互动时可能会用到的方法:
contains(String key)- 检查是否存在给定键的值。remove(String key)- 移除给定键的值。keys()- 返回SavedStateHandle中包含的所有键。
此外,您还可以使用可观察数据存储器从 SavedStateHandle 检索值。支持的类型包括:
StateFlow
您可以从封装在 StateFlow 可观测对象中的 SavedStateHandle 检索值。您可以根据是否需要直接更改值来选择只读或可变数据流:
getStateFlow():如果您只需要读取状态,请使用此方法。当您在SavedStateHandle的其他位置更新键的值时,StateFlow 会收到新值。如果您想公开只读流并使用 Flow 运算符对其进行转换,那么这种方法非常理想。getMutableStateFlow():如果您需要同时拥有读取和写入权限,请使用此权限。更新返回的MutableStateFlow的.value会自动更新底层SavedStateHandle,从而省去手动设置键的麻烦。
通常,您会因用户互动而更新这些值,例如输入查询来过滤数据列表。
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { // Use getMutableStateFlow to read and write the query directly private val _query = savedStateHandle.getMutableStateFlow("query", "") val query: StateFlow= _query.asStateFlow() // Use getStateFlow if you only need a read-only stream to react to changes val filteredData: StateFlow<List > = query.flatMapLatest { repository.getFilteredData(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun setQuery(newQuery: String) { // Updating the MutableStateFlow automatically updates the SavedStateHandle _query.value = newQuery } }
KotlinX 序列化支持
对于复杂的界面状态,您可以将 saved 属性委托与 KotlinX Serialization 结合使用。借助此委托,您可以将自定义 @Serializable 数据类直接持久化到 SavedStateHandle 中。这样一来,即使进程终止,ViewModel 的状态也会保留,因此 Compose 界面可以在重新创建时无缝恢复其状态。
如需使用它,请使用 @Serializable 注释您的数据类,并在 ViewModel 中使用 saved 委托:
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel // Ensure you have the savedstate-ktx dependency import androidx.savedstate.serialization.saved import kotlinx.serialization.Serializable @Serializable data class UserFilterState( val searchQuery: String, val minAge: Int, val includeInactive: Boolean ) class FilterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { // The state is automatically serialized to a Bundle on process death, // and deserialized upon recreation. var filterState by savedStateHandle.saved { UserFilterState(searchQuery = "", minAge = 18, includeInactive = false) } fun updateQuery(newQuery: String) { // Mutating the property automatically updates the underlying SavedStateHandle filterState = filterState.copy(searchQuery = newQuery) } }
Compose 状态支持
如果您的状态依赖于 Compose 的 Saver API,而不是 KotlinX Serialization,则 lifecycle-viewmodel-compose 制品会提供 saveable 委托。这样一来,SavedStateHandle 和 Compose 的 Saver 之间便可实现互操作性,以便您能够通过 rememberSaveable 使用自定义 Saver 保存的任何 State 也可以通过 SavedStateHandle 保存。
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: List<String> by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData += query } } }
支持的类型
保存在 SavedStateHandle 中的数据将作为 Bundle 与应用的 savedInstanceState 的其余部分一起保存和恢复。
直接支持的类型
默认情况下,您可以对 SavedStateHandle 调用 set() 和 get(),以处理与 Bundle 相同的数据类型,如下所示。
| 类型/类支持 | 数组支持 |
double |
double[] |
int |
int[] |
long |
long[] |
String |
String[] |
byte |
byte[] |
char |
char[] |
CharSequence |
CharSequence[] |
float |
float[] |
Parcelable |
Parcelable[] |
Serializable |
Serializable[] |
short |
short[] |
SparseArray |
|
Binder |
|
Bundle |
|
ArrayList |
|
Size (only in API 21+) |
|
SizeF (only in API 21+) |
如果该类没有扩展上述列表中的任何一项,则应考虑通过添加 @Parcelize Kotlin 注解或直接实现 Parcelable 来使该类变为 Parcelable 类。
保存非 Parcelable 类
如果某个类未实现 Parcelable 或 Serializable 且不能修改为实现这些接口之一,则无法直接将该类的实例保存到 SavedStateHandle 中。
从 Lifecycle 2.3.0-alpha03 开始,SavedStateHandle 允许您保存任何对象,具体方法是:使用 setSavedStateProvider() 方法提供您自己的逻辑用于将对象作为 Bundle 来保存和恢复。SavedStateRegistry.SavedStateProvider 是一个接口,用于定义单个 saveState() 方法来返回包含您希望保存的状态的 Bundle。当 SavedStateHandle 准备好保存其状态后,它会调用 saveState() 以从 SavedStateProvider 检索 Bundle,并为关联的键保存 Bundle。
设想一个示例应用,该应用通过 ACTION_IMAGE_CAPTURE intent 向相机应用请求图片,并传递一个临时文件,以供相机存储图片。TempFileViewModel 封装了创建该临时文件的逻辑。
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
为确保临时文件在 activity 的进程终止随后又恢复后不会丢失,TempFileViewModel 可以使用 SavedStateHandle 保留其数据。如需允许 TempFileViewModel 保存其数据,请实现 SavedStateProvider,并在 ViewModel 的 SavedStateHandle 上将其设置为提供程序:
private fun File.saveTempFile() = bundleOf("path", absolutePath) class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
如需在用户返回时恢复 File 数据,请从 SavedStateHandle 中检索 temp_file Bundle。这正是 saveTempFile() 提供的包含绝对路径的 Bundle。该绝对路径随后可用于实例化新的 File。
private fun File.saveTempFile() = bundleOf("path", absolutePath) private fun Bundle.restoreTempFile() = if (containsKey("path")) { File(getString("path")) } else { null } class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { val tempFileBundle = savedStateHandle.get<Bundle>("temp_file") if (tempFileBundle != null) { tempFile = tempFileBundle.restoreTempFile() } savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
测试中的 SavedStateHandle
如需测试将 SavedStateHandle 作为依赖项的 ViewModel,请使用所需的测试值创建一个 SavedStateHandle 新实例,并将其传递给要测试的 ViewModel 实例。
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
其他资源
如需详细了解 ViewModel 的已保存状态模块,请参阅以下资源。
Codelab
查看内容
为你推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 保存界面状态
- 使用可观察的数据对象
- 创建具有依赖项的 ViewModel