导航与状态保存:处理配置变更、保存Fragment状态、使用SavedStateHandle
状态保存这件事,说简单也简单,说复杂也复杂。我刚开始用Navigation组件时,就踩过一个坑——手机转了一下屏幕,Fragment里的数据全没了。用户填了一半的表单,瞬间回到解放前。那会儿我就在想,这玩意儿到底该怎么玩?
今天咱们就好好聊聊这个话题。配置变更、Fragment状态保存、SavedStateHandle,这三者其实是同一件事的不同侧面。你搞懂了它们的关系,状态管理就不再是玄学。
配置变更:绕不开的坎
什么是配置变更?说白了就是屏幕旋转、语言切换、字体大小调整这类事。系统会销毁当前Activity,然后重建一个。你想想看,用户正看着你的App,突然屏幕一转,界面没了,数据丢了,这体验能好吗?
Android官方给了几种方案:
- onSaveInstanceState:最传统的方式,适合保存少量轻量数据
- ViewModel:配置变更时不会销毁,天然适合保存UI状态
- 非配置保留Fragment:老方案,现在基本被ViewModel取代
我个人习惯用ViewModel来处理大部分场景。为什么?因为它生命周期长,而且和Navigation组件配合得天衣无缝。
核心原则:能放ViewModel的数据,就别往onSaveInstanceState里塞。ViewModel是配置变更的避风港。
Fragment状态保存:别让用户白填
Fragment的状态保存,其实分两种情况:
- 配置变更导致的重建:屏幕旋转这类
- Fragment被系统回收:App切到后台,内存不足时
第一种情况,ViewModel就能搞定。第二种情况,就得靠SavedStateHandle了。
我在项目中遇到过一个问题:用户在一个多步骤表单里填了5页数据,突然接了个电话,回来发现App被杀了。重启后数据全没了。用户气得直接给了差评。从那以后,我对状态保存就格外上心。
SavedStateHandle:ViewModel的救星
SavedStateHandle是ViewModel和SavedInstanceState之间的桥梁。它让你在ViewModel里也能享受到状态保存的好处。
用法很简单:
class MyViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 像普通LiveData一样使用
val name = savedStateHandle.getLiveData<String>("name", "")
fun updateName(newName: String) {
savedStateHandle["name"] = newName
}
}
你可能会问:这和直接在ViewModel里用LiveData有什么区别?
区别大了。普通LiveData在进程被杀后数据就丢了。但SavedStateHandle会把数据存到Bundle里,进程恢复时自动还原。说白了,它帮你做了序列化和反序列化的脏活累活。
小技巧:SavedStateHandle支持的数据类型和Bundle一样。如果你要存自定义对象,记得实现Parcelable或Serializable。我个人推荐Parcelable,性能更好。
Navigation组件中的状态保存
Navigation组件对状态保存有天然的支持。每个NavBackStackEntry都关联了一个ViewModelStore和一个SavedStateRegistry。这意味着:
- 导航到目标Fragment时,ViewModel会自动创建
- 返回时,ViewModel不会销毁(除非栈被清空)
- 配置变更时,ViewModel存活,数据不丢
但这里有个坑:如果你用Navigation的popUpTo清除了某个目标,它的ViewModel也会被清除。所以不要指望ViewModel能永久保存数据。
注意:SavedStateHandle保存的数据量不宜过大。Bundle有1MB的大小限制,超过这个值会抛出TransactionTooLargeException。我曾经见过有人把整个列表数据塞进去,结果App直接崩溃。大块数据请用本地数据库或文件存储。
实战:一个完整的例子
咱们来看一个实际场景:用户填写个人信息,包含姓名、年龄、地址。屏幕旋转后数据不能丢,进程被杀后也要能恢复。
// 1. 定义ViewModel
class ProfileViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val name = savedStateHandle.getLiveData<String>("name", "")
val age = savedStateHandle.getLiveData<Int>("age", 0)
val address = savedStateHandle.getLiveData<String>("address", "")
fun saveName(value: String) {
savedStateHandle["name"] = value
}
fun saveAge(value: Int) {
savedStateHandle["age"] = value
}
fun saveAddress(value: String) {
savedStateHandle["address"] = value
}
}
// 2. 在Fragment中使用
class ProfileFragment : Fragment() {
private val viewModel: ProfileViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 观察数据变化
viewModel.name.observe(viewLifecycleOwner) { name ->
binding.nameInput.setText(name)
}
// 保存数据
binding.saveButton.setOnClickListener {
viewModel.saveName(binding.nameInput.text.toString())
}
}
}
这段代码看起来平平无奇,但它的威力在于:不管你怎么旋转屏幕,甚至App被系统杀掉,用户填的数据都能恢复。你想想看,用户会怎么评价你的App?
知识体系总览
下面这张图,帮你理清今天讲的所有概念之间的关系:
从这张图可以看得很清楚:配置变更触发后,你有三条路可走。但最推荐的是ViewModel + SavedStateHandle的组合。它既解决了配置变更的问题,也解决了进程被杀的问题。
避坑指南
最后,分享几个我踩过的坑:
- 不要滥用SavedStateHandle:它适合保存UI状态,不适合保存业务数据。业务数据请用Room或DataStore。
- 注意LiveData的粘性事件:SavedStateHandle的LiveData在恢复时会立即发射旧值。如果你不想让用户看到旧数据,记得做去重处理。
- Fragment的View和ViewModel生命周期不同:View在onDestroyView后就不能用了,但ViewModel还在。所以观察LiveData时要用viewLifecycleOwner,而不是this。
嗯,状态保存这件事,说白了就是让用户觉得你的App很「稳」。用户不会在意你用了什么技术,他们只关心数据会不会丢。你把这些做好了,用户自然就留住了。
公众号:蓝海资料掘金营,微信deep3321