本章主要学习使用ViewModel保存UI数据,修复GeoQuiz应用的UI状态丢失缺陷。
ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。
它来自lifecycle-extensions的Android Jetpack库,目前 lifecycle-extensions 中的 API 已弃用。您可以为特定 Lifecycle 工件添加所需的依赖项。参考:「https://developer.android.com/jetpack/androidx/releases/lifecycle#declaring_dependencies」
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-alpha03'
然后点击 Sync Now。
创建 QuizViewModel
private const val TAG = "QuizViewModel"
class QuizViewModel : ViewModel() {
init {
Log.d(TAG, "ViewModel instance created")
}
/**
* On cleared
* onCleared()函数的调用恰好在ViewModel被销毁之前。适合做一些善后清理工作,比如解绑某个数据源。
*/
override fun onCleared() {
super.onCleared()
Log.d(TAG, "ViewModel instance about to destroyed")
}
}
访问ViewModel
书中访问ViewModel的方法已经被弃用了,正如前面所说,我的实践并非引入 lifecycle-extensions,因此实际代码有所小改动。
在MainActivity.class 的 onCeate()方法中加入:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
val quizViewModel by lazy { ViewModelProvider(this).get(QuizViewModel::class.java) }
...
}
只是换了api去拿quizViewModel的实例而已。
在MainActivity首次访问QuizViewModel时,ViewModelProvider会创建并返回一 个QuizViewModel新实例。在设备配置改变之后,MainActivity再次访问QuizViewModel对象时,它返回的是之前创建的QuizViewModel。在MainActivity完成使命销毁时(比如用户按了回退键),ViewModel-Activity这对好朋友也就从内存里抹掉了。
上述代码意味着,一个ViewModel实例和一个activity生命周期已经关联,不管关联activity处于什么状态,该ViewModel会一直保留在内存里,直到关联activity被销毁。
QuizViewModel和MainActivity步调一致
设备旋转时,ViewModel 也留在了内存里。
MainActivity和QuizViewModel经历设备旋转
运行GeoQuiz应用日志:
初次打开
旋转设备日志:(可以看出viewmodel并未重建,而是从内存中直接取第一次创建的)
旋转后
退出应用日志:(viewmodel才销毁)
退出app
小总结:QuizViewModel 和 MainActivity 的关系是单向的。某个activity会引用其关联ViewModel,反过来则不行。一个ViewModel绝不能引用activity或view,否则会引发内存泄漏。
当某个对象强引用另一个要被销毁的对象时,内存泄漏就会发生。这样的强引用会阻止垃圾回收器从内存里清理对象。设备配置改变带来的内存泄漏是常见问题。
ViewModel 会保存关联用户界面所需数据,并整理格式化这些数据,以方便其他对象取用。这样就可以把屏幕展现逻辑从activity里删除,让其“瘦身”了。
class QuizViewModel : ViewModel() {
var currentIndex = 0
private val questionBank = listOf(
Question(R.string.question_australia, true),
Question(R.string.question_oceans, true),
Question(R.string.question_mideast, false),
Question(R.string.question_africa, false),
Question(R.string.question_americas, true),
Question(R.string.question_asia, true)
)
val currentQuestionAnswer: Boolean get() = questionBank[currentIndex].answer
val currentQuestionText: Int get() = questionBank[currentIndex].textResId
fun moveToNext() {
currentIndex = (currentIndex + 1) % questionBank.size
}
}
使用by lazy关键字,可以确保quizViewModel属性是val类型,而不是var类型。只在activity实例对象被创建后,才需要获取和保存QuizViewModel,也就是说,quizViewModel一次只赋一个值。
上面讲述的是发生屏幕旋转等配置更改的情况下,activity会被销毁和重启,这个时候可以用viewmodel来自动保存数据与获取数据。但是,如果是整个Android系统内存不够用的情况下,app又不在前台,系统是可能直接清除掉整个app的进程,这个时候,viewmodel 就不管用了,因为它也不在了。
通过覆盖Activity.onSaveInstanceState(Bundle)的方式,就可以解决上述问题,当应用进程在意外被系统“杀死”的时候,帮用户保存一些不是很大的关键数据,从而在再次加载app的时候恢复状态。
增加一个暂存状态(stashed state)到activity生命周期:
完整的activity生命周期
注意,activity进入暂存状态并不一定需要调用onDestroy()函数。不过,onStop()和onSaveInstanceState(Bundle)是两个可靠的函数(除非设备出现重大故障)。
通常,覆盖onSaveInstanceState(Bundle)函数,在Bundle对象中,保存当前activity小的或暂存状态的数据;覆盖onStop()函数,保存永久性数据,比如用户编辑的文字等。
要测试系统内存不够杀死应用,进入开发者选项,将不保留活动开启,那么在应用启动后,点击了home键,系统就是自动去杀死app了。如图设置:
不保留活动
保留实例状态和ViewModel都不是长期存储解决方案。如果应用需要长久存储数据,且完全不担心activity状态,那么请考虑使用持久化存储方案。(后续会学)
ViewModel 始终还是对内存数据进行操作,所以速度上来说会占优势,加上书中的GeoQuiz应用例子,题目都是硬编码的,不是从网络获取,而且数据也不多,不需要数据库来存储,因此对于此应用来说,使用ViewModel是方案还是很合理的。
因此,要处理设备配置更改 加上 系统发起的进程终止 两种情况,就结合使用 ViewModel 和 onSaveInstanceState() 方式来保存数据状态。
Jetpack库分为四大类:foundation、architecture、behavior和UI。
architecture类Jetpack库还有一个常见名字叫architecture component。ViewModel就是一种架构组件。
参考:https://developer.android.com/jetpack
意思就是通过禁止应用屏旋转,以此解决设备配置改变带来的UI状态丢失问题的方式太粗暴,也不能从根本解决问题,这也解决不了决进程销毁问题,在开发过程中,还会遇到其他的跟生命周期有关的问题,我们得查到根本,然后多学一些知识技术点,来解决开发问题!
本篇的实践代码地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/GeoQuiz
由于会按照自己的意思实践练习题,因此跟书中示例代码有少许不一样,仅供参考。有兴趣就自行完善喽。实践出真知呢!给自己加个油!🆙