赞
踩
来来来,继续学穿Jetpack,本节带来组件 → ViewModel
视图模型的解读!叫 视图数据 可能更贴切,有人也叫 视图状态,都一个意思,怎么称呼看你自己喜欢~
ViewModel 将 视图数据
从 视图控制器
中分离,并实现了 数据管理
的:一致性
、数据共享(跨页面通信)
及 作用域可控
。
视图控制器
一般代指Activity和Fragment,它们通过在屏幕上绘制View,捕获用户事件,处理用户与互动界面相关的操作来 控制界面。
视图数据
就是你用来对控件setXxx()的数据源,它和与它相关的决策逻辑 (或者说管理) 不应该放到视图控制器中。
ViewModel所做的事,就是用 模版方法模式 进行封装,隐藏一些具体细节,提供简洁的API供我们使用。给了我们一种它们好像真的分离了的错觉,实际上还是与视图控制器紧密相连,ViewModel依旧被对应的Activity、Fragment所持有。
最直观的体现::页面配置变更,引起页面销毁重建,ViewModel中的数据不会因此而丢失
。
写个简单的例子更直观,先不用ViewModel:
class TestActivity : AppCompatActivity() {
private var mCount: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
tv_content.text = "${mCount}"
}
bt_test.setOnClickListener { tv_content.text = "${++mCount}" }
}
操作:点击按钮mCount会自增1,旋转手机触发屏幕翻转,发现数字又从0开始了:
接着用上ViewModel,定义一个类继承ViewModel,把mCount丢到里面:
class TestViewModel: ViewModel() { var mCount = 0 }
改动下原代码:
class TestActivity : AppCompatActivity() {
private lateinit var testViewModel: TestViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
testViewModel = ViewModelProvider(this).get(TestViewModel::class.java)
tv_content.text = "${testViewModel.mCount}"
}
bt_test.setOnClickListener { tv_content.text = "${++testViewModel.mCount}" }
}
执行同样的操作,无论怎么翻转,数字都不会因页面重建而丢失(表现为从0开始)。
结论:把视图数据放到ViewModel里,就不会受页面配置变化销毁重建的影响。
对了,上面说到的配置变更,除了横竖屏切换外,还有这些:
分辨率调整、权限变更、系统字体样式变更、系统语言切换、多窗口设定、系统导航方式变更等。
在以前,为了避免这种 页面配置变更引起的页面销毁重建
导致的 视图数据丢失
问题,需要我们在 onSaveInstanceState()
和 onRestoreInstanceState()
手动编写数据保存和恢复的语句。
页面一多、要保存恢复的数据一多、加之多人协作,就很容易出现 结果不一致的问题,比如:某人在编写存数据相关的代码,漏掉了某个数据,导致拿时没拿到正确的数据。
结论:使用ViewModel,你只管把数据丢里面就行,无需关心具体如何存取,间接保证了结果一致性。
答:非也非也,具体用哪个还得权衡数据复杂度、访问速度及生命周期,建议 混合使用,分而治之。
怎么说?除了这两种存储恢复数据的方式外,还有一种 持久化存储,官方文档 《保存界面状态》 提供了一个维度参考表:
简要说下笔者的看法:
ViewModel
onSaveInstanceState()
持久性存储
如果 数据的恢复非常重要、存储数据非常大、数据需要长期存储 的场景,可以考虑持久化存储,比如存数据库中。建议策略:间歇性提前自动把临时数据从内存中备份到硬盘中。当然,持久性存储不局限于本地,网络亦可。
日常开发中,Activity和Fragment通信,Fragment与Fragment通信的场景非常常见,常见的做法下述几种:
各有利弊,而采用ViewModel,只需 指定作用域,即可轻松实现跨页面通信。写个烂大街的经典例子:点击列表Fragment,更新右侧内容Fragment,预期效果如下:
接着写代码实现一波,先是左侧列表项的布局 (item_list.xml):
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_choose"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@android:color/holo_green_light"
android:gravity="center" />
接着到列表适配器类 (ListAdapter.kt)
class ListAdapter(data: ArrayList<String>): RecyclerView.Adapter<ListAdapter.ViewHolder>() { private var mData = data private var mClickListener: ItemClickListener? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent ,false)) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.chooseTv?.let { it.text = mData[position] it.setOnClickListener { mClickListener?.onItemClick(mData[position]) } } } override fun getItemCount() = mData.size fun setOnItemClickListener(listener: ItemClickListener) { this.mClickListener = listener } inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { var chooseTv: TextView? = null init { chooseTv = itemView.findViewById(R.id.tv_choose) } } interface ItemClickListener { fun onItemClick(choose: String) } }
设置文本,预留点击接口而已,再接着到列表Fragment的布局,直接一个RecyclerView (fragment_list.xml)
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
接着自定义ViewHolder,维护一个选中的值,配置LiveData:
class SharedViewModel: ViewModel() {
private val mSelectData = MutableLiveData<String>()
fun select(data: String) { mSelectData.value = data }
fun getSelected() = mSelectData
}
再接着把ListFragment也写出来,获取宿主Activity作用域的SharedViewModel实例,点击时更新值:
class ListFragment(data: ArrayList<String>): Fragment(R.layout.fragment_list) { private var mData = data // 定义SharedViewModel变量 private var mModel: SharedViewModel? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 获得宿主Activity作用域内的SharedViewModel实例 mModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java) view.findViewById<RecyclerView>(R.id.rv_list).apply { adapter = ListAdapter(mData).also { it.setOnItemClickListener(object : ListAdapter.ItemClickListener { override fun onItemClick(choose: String) { // 更新ViewModel中的mSelectData mModel?.select(choose) } }) } layoutManager = LinearLayoutManager(activity) } } }
在接着到右侧Fragment,xml里就一个简单的TextView:(fragment_content.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_light">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"/>
</LinearLayout>
补齐ContentFragment:
class ContentFragment : Fragment(R.layout.fragment_content) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 获取宿主Activity作用域的SharedViewModel实例,然后监听数据变化
ViewModelProvider(requireActivity()).get(SharedViewModel::class.java).getSelected()
.observe(viewLifecycleOwner) {
view.findViewById<TextView>(R.id.tv_content).text = "您翻牌了:${it}"
}
}
}
紧着是测试Activity的xml (activity_vm_test.xml):
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <FrameLayout android:id="@+id/fly_choose" android:layout_width="wrap_content" android:layout_height="match_parent" /> <FrameLayout android:id="@+id/fly_content" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"/> </LinearLayout>
最后上测试Activity:
class VMTestActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_vm_test)
val mData = arrayListOf("XX","XX","XX","XX","XXX", "XX", "XX", "XX")
supportFragmentManager.apply {
beginTransaction().replace(R.id.fly_choose, ListFragment(mData)).commit()
beginTransaction().replace(R.id.fly_content, ContentFragment()).commit()
}
}
}
别看这里代码好像很多的样子,只是为了便于读者copy运行体验而已,核心就这四点:
通过这样的方式,间接实现了跨页面通信,宿主Activity还蒙在鼓里 (无代码入侵),两个Fragment就偷偷完成了py交易,妙啊!!!
上面两个Fragment轻松实现数据共享的例子,得益于ViewModel的 作用域可控,在创建 ViewModelProvider
时注入不同的 ViewModelStoreOwner
来反映作用域。
如果换成传入 Fragment实例本身,作用域就 仅限于此Fragment,当此Fragment销毁时,对应的ViewModel实例也会被销毁。
看一波源码探探原理,先跟下 ViewModelProvider
的构造方法:
初始化了一个Factory和ViewModelStore,先看第一个参数,调用了 owner.getViewModelStore()
此方法返回一个 ViewModelStore
,跟下:
吼,内部 维护一个ViewModel的集合,还提供一个clear()方法,遍历回调ViewModel的clear()方法,并清空集合。
所以包含关系是:ViewModelProvider
→ ViewModelStore
→ ViewModel
接着看第二个参数,根据传入的ViewModelStoreOwner不同,使用不同的工厂实例:
HasDefaultViewModelProviderFactory
类型?getDefaultViewModelProviderFactory()
获取一个 ViewModelProvider.Factory
实例;NewInstanceFactory.getInstance()
获取一个 NewInstanceFactory
实例;构造方法中初始化了Factory和ViewModelStore实例,继续往下走,跟下 get()
方法:
在此拼接了一个 key
,继续跟另一个 get()
方法:
这里执行的操作非常简单明了:
到此好像还没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的特点大概了解到这,接着过下基本用法~
老规矩,官方文档双手奉上:《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
扫一波就知道了
上面例子已经写得很明显了,就不重复了,这里提两点:
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() 方法,以便它可以清理资源。
ViewModel将一直存在与内存中,直到限定其时间访问的Lifecycle永久消失:对于Activity,是在Activity finish时,对于Fragment,是在Fragment移除时。基本用法就这些,协程搭配Jetpack组件使用,后面会专门讲~
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()
的数据最终存储到 ActivityManagerService
的 ActivityRecord
中,即 系统进程,所以APP被杀后还能恢复;onRetainNonConfigurationInstance()
数据是存储到 ActivityClientRecord
中,即 应用自身进程中 ,所以APP被杀后无法恢复。另外,再送一个问题:ViewModelStore的onCleard()何时会被调用?
关于原理相关的就只了解到这吧,Activity销毁重建完整逻辑可是个大块头,就不展开讲了~
上面说过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销毁重建,效果一致:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EM3EipRg-1654522034819)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1bc8e194153e4abeb9d7e47c03b2067e~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image)]
接着试下杀掉进程,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的数据。
本节过了下ViewModel的用法,对它的特点:视图数据与控制器、数据管理的一致性、数据共享、作用域可控进行了详解的解读,并配以简单例子帮助理解,还从源码层面讲解了ViewModel自动保存和恢复的原理,最后还提了一嘴ViewModel-State组件的使用。基本上算是面面俱到了,相信看完的读者用起ViewModel来也是水到渠成了。
有问题或者建议欢迎在评论区提出,肝文不易,如果本文有帮到你的话,可以给个三连,谢谢~
要想成为架构师,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……
1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程
1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》
从一个膜拜大神的 Demo 开始
Kotlin 写 Gradle 脚本是一种什么体验?
Kotlin 编程的三重境界
Kotlin 高阶函数
Kotlin 泛型
Kotlin 扩展
Kotlin 委托
协程“不为人知”的调试技巧
图解协程:suspend
1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习
1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)
…
1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取【保证100%免费】↓↓↓
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。