【Jetpack】学穿:ViewModel → 视图模型(下)

简介: 本节带来组件 → ViewModel 视图模型的解读!叫 视图数据 可能更贴切,有人也叫 视图状态

构造方法中初始化了Factory和ViewModelStore实例,继续往下走,跟下 get() 方法:


网络异常,图片无法展示
|


在此拼接了一个 key,继续跟另一个 get() 方法:


网络异常,图片无法展示
|


这里执行的操作非常简单明了:


  • 拼接key 区分不同作用域的ViewModel实例,规则:固定字符串+ViewModel完整类名;
  • ② 尝试从 缓存Map 中根据key获取ViewModel实例;
  • ③ 拿到:直接返回
  • ④ 拿不到:往下走,通过工厂创建新ViewModel实例,保存到缓存Map中,然后返回;


到此好像还没get√到具体怎么实现作用域可控?


把关注点拉回 owner.getViewModelStore() 上,这个owner是 ViewModelStoreOwner 类型的,而我们上面传入的是 Activity实例,可以推测Actitivty绝壁实现了这个接口。定位一波:


网络异常,图片无法展示
|


跟下 getViewModelStore()


网络异常,图片无法展示
|


网络异常,图片无法展示
|


2333,原来是 ComponentActivity 内部自己维护了一个ViewModelStore。

啧啧,再来看看Fragment又是怎么玩的,跟下 Fragment.getViewModelStore()


网络异常,图片无法展示
|


跟下 FragmentManager.getViewModelStore()


网络异常,图片无法展示
|


网络异常,图片无法展示
|


Fragment 内部持有一个 FragmentManagerViewModel 实例,点进去它的 getViewModelStore() 方法:


网络异常,图片无法展示
|


网络异常,图片无法展示
|


网络异常,图片无法展示
|


好家伙,我悟了:Fragment → FragmentManager → FragmentManagerViewModel → mViewModelStores集合


所以:ViewModel的作用域可控 = 工厂模式 + 缓存集合(特定key规则)


关于ViewModel的特点大概了解到这,接着过下基本用法~


0x2、ViewModel 基本用法


老规矩,官方文档双手奉上:《ViewModel 概览》,以官方文档和源码为准~


① 依赖组件


ViewModel基本配合LiveData使用,更多依赖可以选择可参见:Lifecycle


def lifecycle_version = "2.4.1"
// Java项目
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
// Kotlin项目
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"


咳,如果你启用了DataBinding,可以不用另外依赖,不然会发现两个版本的ViewModel:


网络异常,图片无法展示
|


不信的话,自己命令行键入:gradlew :app:dependencies > dependencies.txt 扫一波就知道了


② 实现ViewModel


上面例子已经写得很明显了,就不重复了,这里提两点:


  1. 如果依赖了 activity-ktx 模块,可以使用 by viewModels() 委托初始化ViewModel。


implementation 'androidx.activity:activity-ktx:1.4.0'
val model: MyViewModel by viewModels()
// Tips:如果依赖了fragment-ktx模块,可以在Fragment中商用activityViewModels() 委托初始化
// 宿主Activity的ViewModel
implementation 'androidx.fragment:fragment-ktx:1.4.1'
private val model: SharedViewModel by activityViewModels()


当所有者Activity关闭时,会调用ViewModel对象的onCleared() 方法,以便它可以清理资源。


  1. ViewModel绝不能引用View、Lifecycle或可能存储对Activity上下文的引用的类内存泄露警告!!!


③ ViewModel的生命周期

网络异常,图片无法展示
|


ViewModel将一直存在与内存中,直到限定其时间访问的Lifecycle永久消失:对于Activity,是在Activity finish时,对于Fragment,是在Fragment移除时。基本用法就这些,协程搭配Jetpack组件使用,后面会专门讲~


0x3、面试题:ViewModel自动保存和恢复的原理


ViewModel好像就这个能问了,简单的探一探,使用 ViewModelProvider 实例化ViewModel时,传入 ViewModelStoreOwner 对象作为参数,Activity、Fragment自然实现了这个接口。跟下:ComponentActivity.getViewModelStore()

网络异常,图片无法展示
|


跟下 ensureViewModelStore


网络异常,图片无法展示
|


点开 NonConfigurationInstances,可以看到 ViewModelStore 对象被缓存在这里:


网络异常,图片无法展示
|


跟下 getLastNonConfigurationInstance()


网络异常,图片无法展示
|


就是Activity除了提供 onSaveInstanceState()onRestoreInstanceState() 外,还另外提供了两个方法 onRetainNonConfigurationInstance()getLastNonConfigurationInstance() 专门处理配置更改。


跟下 onRetainNonConfigurationInstance()


网络异常,图片无法展示
|


就是在配置更改销毁重建过程中,先调用 onRetainNonConfigurationInstance() 保存 旧Activity中的ViewModelStore实例,重建后通过 getLastNonConfigurationInstance() 获取到之前保存的ViewModelStore实例。


知道怎么保存和恢复,接着就是确定 调用时机 ,跟下 ActivityThread.performDestroyActivity() 它是 Activity销毁 调用的核心实现:


网络异常,图片无法展示
|


跟下 Activity.retainNonConfigurationInstances()


网络异常,图片无法展示
|


知道保存数据的方法是在这里调用的,接着看获取数据的方法又是在哪调用的,跟下 Activity.handleLaunchActivity(),它是 Activity启动 的重要步骤:


网络异常,图片无法展示
|


还记得销毁处的代码吗:


网络异常,图片无法展示
|


销毁时,先存到 ActivityClientRecord.lastNonConfigurationInstances 中,然后在Activity启动时,通过 attach() 方法传递给新Activity。


网络异常,图片无法展示
|


到此就一清二楚了,onSaveInstanceState() 相关的也可以在 ActivityThread() 中找到踪迹,如:


网络异常,图片无法展示
|


就不去跟了,除了前面说的存数据的颗粒度大小不同外,两者还存在下述区别:


  • onSaveInstanceState() 的数据最终存储到 ActivityManagerServiceActivityRecord 中,即 系统进程,所以APP被杀后还能恢复;
  • onRetainNonConfigurationInstance() 数据是存储到 ActivityClientRecord 中,即 应用自身进程中 ,所以APP被杀后无法恢复。


另外,再送一个问题:ViewModelStore的onCleard()何时会被调用


网络异常,图片无法展示
|


关于原理相关的就只了解到这吧,Activity销毁重建完整逻辑可是个大块头,就不展开讲了~


0x4、加餐:ViewModel-State组件


上面说过ViewModel仅对页面变更,Activity销毁后打开重建只能用onSaveInstanceState(),写个简单例子验证下:


class VMFirstActivity: AppCompatActivity() {
    companion object {
        const val COUNT_TAG = "count"
    }
    private var mSaveInstanceCount = 0
    private val mModel: VMViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_wm_first)
        // 判断是否有保存的数据,有取值
        savedInstanceState?.let { mSaveInstanceCount = it.getInt(COUNT_TAG) }
        tv_vm_content.text = "ViewModel保存的数据:${mModel.mCount}"
        tv_on_save_content.text = "onSaveInstanceState()保存的数据:${mSaveInstanceCount}"
        bt_test.setOnClickListener {
            tv_vm_content.text = "ViewModel保存的数据:${++mModel.mCount}"
            tv_on_save_content.text = "onSaveInstanceState()保存的数据:${++mSaveInstanceCount}"
        }
    }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(COUNT_TAG, mSaveInstanceCount)
    }
}
class VMViewModel: ViewModel() { var mCount = 0 }


运行后点击按钮自增多次,然后旋转手机引起Activity销毁重建,效果一致:


网络异常,图片无法展示
|


接着试下杀掉进程,home键退到后台,键入下述命令:


adb shell am kill 应用包名


点击桌面图标重新打开:


网络异常,图片无法展示
|


果然,ViewModel中的数据丢失了,如果数据比较重要,而且量不大,可以在onCreate()拿到savedInstanceState时也重置一下值。


savedInstanceState?.let {
    mSaveInstanceCount = it.getInt(COUNT_TAG)
    mModel.mCount = it.getInt(COUNT_TAG)
}


然后就可以了,当然,这样搞法有点冗余,如果能判断是配置变更引起的重建,还是异常销毁引起的重建就好了,笔者暂时没找到判定的API,只能这样了。有知道的小伙伴欢迎在评论区告知~


对于这种场景,Jetpack其实还给我们提供了一个模块:SaveState,activity库内部默认引入了这个组件,不需要另外依赖,当然你要依赖特定版本也是可以的:


implementation "androidx.savedstate:savedstate:1.0.0"


用法非常简单,ViewModel的构造方法,传入一个SavedStateHandle参数,然后用这个参数读取数据即可:


class VMViewModel(private val state: SavedStateHandle): ViewModel() {
    private val countTag = "count"
    fun setValue(value: Int) = state.set(countTag, value)
    fun getValue() = state.get<Int>(countTag)
}


把原先onSaveInstanceState相关的代码干掉后:


class VMFirstActivity: AppCompatActivity() {
    private val mModel: VMViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_wm_first)
        // 如果为空,要先设置一个初始值,不然一直都是null
        if(mModel.getValue() == null) mModel.setValue(0)
        tv_vm_content.text = "ViewModel保存的数据:${mModel.getValue()}"
        bt_test.setOnClickListener {
            // 需要判空,然后在更新值
            mModel.getValue()?.plus(1)?.let { mModel.setValue(it) }
            tv_vm_content.text = "ViewModel保存的数据:${mModel.getValue()}"
        }
    }
}


清爽多了,除了可以调用 get() 方法外,还可以调用 getLiveData() 来获取LiveData类型的值。代码就更精简了:


网络异常,图片无法展示
|


非常简单,不过要注意下:


存数据跟Bundle一样,存对象要序列化,然后也适合保存轻量级的数据!!!


原理的话,还是利用的 onSaveInstanceState(),每个ViewModel的数据单独存在一个Bundle中,再合并成一个整体,放到outBundle,所以它同样不能存超过1M的数据。

具体源码和流程就不去刨了,感兴趣的可自行查阅下述两篇文章:



0x5、小结


本节过了下ViewModel的用法,对它的特点:视图数据与控制器、数据管理的一致性、数据共享、作用域可控进行了详解的解读,并配以简单例子帮助理解,还从源码层面讲解了ViewModel自动保存和恢复的原理,最后还提了一嘴ViewModel-State组件的使用。基本上算是面面俱到了,相信看完的读者用起ViewModel来也是水到渠成了。


参考文献:



相关文章
|
3月前
|
前端开发 测试技术 API
Jetpack MVVM 七宗罪之六:ViewModel 接口暴露不合理
Jetpack MVVM 七宗罪之六:ViewModel 接口暴露不合理
43 0
|
8月前
|
Android开发
Android JetPack组件之ViewModel状态的保存(程序在后台被系统杀死数据也存活)
Android JetPack组件之ViewModel状态的保存(程序在后台被系统杀死数据也存活)
98 0
|
8月前
|
存储 Android开发
Android JetPack组件之ViewModel的使用详解
Android JetPack组件之ViewModel的使用详解
81 0
|
10月前
|
编解码
Jetpack 之 ViewModel 组件介绍
ViewModel 是介于 View(视图)和 Model(数据模型)之间的一个东西。它起到了桥梁的作用,使视图和数据既能够分离开,也能够保持通信。
55 0
Jetpack 之 ViewModel 组件介绍
|
12月前
|
存储 XML 前端开发
Android Jetpack系列之ViewModel
ViewModel的定义:**ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据**。ViewModel本质上是视图(View)与数据(Model)之间的桥梁,想想以前的MVC模式,视图和数据都会写在Activity/Fragment中,导致Activity/Fragment过重,后续难以维护,而ViewModel将视图和数据进行了分离解耦,为视图层提供数据。
167 0
|
XML 存储 Android开发
大放光彩的安卓Jetpack组件-ViewModel(终)
前面我们已经说过Jetpack中ViewModel的作用、用法以及使用要点,但还缺少在Activity中的实例展示,所以本节我们将结合结果展示与代码进行解读,希望能更好的展示出ViewModel的风采。
107 0
|
Android开发
大放光彩的安卓Jetpack组件-ViewModel(二)
上回我们说到使用方法,但没有具体去说明使用的要点,其实ViewMode还是挺容易上手的,这节我们来具体说明一些使用要点与运用方式。
|
前端开发 Android开发
大放光彩的安卓Jetpack组件-ViewModel(一)
在项目中,我遇到了一个问题,起因则是无法实时去获取信息来更新UI界面,因为我需要知道我是否获取到了实时信息
|
Android开发 Windows 容器
浅析 JetPack Compose 是如何安装到View视图上
为什么 Compose 无需在意 view 层级问题,怎样嵌套都行? (最简单10s就能明白);
206 0
浅析 JetPack Compose 是如何安装到View视图上