【Android Jetpack】理解ViewModel

文章目录

  • ViewModel
    • 实现ViewModel
    • ViewModel的生命周期
    • 在Fragments间分享数据
    • ViewModel和SavedInstanceState对比
    • ViewModel原理
    • ViewModel与AndroidViewModel

ViewModel

Android系统提供控件,比如ActivityFragment,这些控件都是具有生命周期方法,这些生命周期方法被系统调用。在数据使用上有两个问题:

  • 当这些控件被销毁或者被重建的时候,如果数据保存在这些对象中,那么数据就会丢失。比如在一个界面,保存了一些用户信息,当界面重新创建的时候,就需要重新去获取数据。当然了也可以使用控件自动再带的方法,在onSaveInstanceState方法中保存数据,在onCreate中重新获得数据,但这仅仅在数据量比较小的情况下。如果数据量很大,这种方法就不能适用了。

  • 另外一个问题就是,经常需要在Activity中加载数据,这些数据可能是异步的,因为获取数据需要花费很长的时间。那么Activity就需要管理这些数据调用,否则很有可能会产生内存泄露问题。最后需要做很多额外的操作,来保证程序的正常运行。

同时Activity不仅仅只是用来加载数据的,还要加载其他资源,做其他的操作,最后Activity类变大,就是我们常讲的上帝类。也有不少架构是把一些操作放到单独的类中,比如MVP就是这样,创建相同类似于生命周期的函数做代理,这样可以减少Activity的代码量,但是这样就会变得很复杂,同时也难以测试。

ViewModel是专门用于存放应用程序页面所需的数据。ViewModel将页面所需的数据从页面中剥离出来,页面只需要处理用户交互和展示数据。

Viewmodel解决两个问题:

  1. 数据的变更与view隔离,也就是解耦
  2. 开发者不需要额外去关心因为屏幕旋转带来的数据恢复问题

实现ViewModel

ViewModel是一个抽象类,其中只有一个onCleared()方法。当ViewModel不再被需要,即与之相关的Activity都被销毁时,onCleared()方法会被系统调用。我们可以在该方法中执行一些资源释放的相关操作。(注意,由于屏幕旋转而导致的Activity重建,并不会调用该方法)

class TimerViewModel : ViewModel() {private val timer: Timer = Timer() 	//定时器类private var timeChangeListener: OnTimeChangeLister? = nullprivate var currentSecond: Int = 0fun start() {timer.schedule(timerTask {currentSecond++timeChangeListener?.onTimeChanged(currentSecond)}, 1000, 1000)}override fun onCleared() {super.onCleared()timer.cancel()}fun setOnTimeChangeListener(listener: OnTimeChangeLister) {timeChangeListener = listener}interface OnTimeChangeLister {fun onTimeChanged(second: Int)}
}
  • ViewModel的实例化过程,是通过ViewModelProvider来完成的。ViewModelProvider会判断ViewModel是否存在,若存在则直接返回,否则它会创建一个ViewModel。
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val tv = findViewById<TextView>(R.id.tv)val model = ViewModelProvider(this).get(TimerViewModel::class.java)//获得ViewModel实例model.setOnTimeChangeListener(object : TimerViewModel.OnTimeChangeLister {override fun onTimeChanged(second: Int) {runOnUiThread {tv.text = second.toString()}}})model.start()}
}

运行程序并旋转屏幕。当旋转屏幕导致Activity重建时,计时器并没有停止。这意味着,横/竖屏状态下的Activity所对应的ViewModel是同一个,它并没有被销毁,它所持有的数据也一直都存在着。

ViewModel的生命周期

  • ViewModel在获取ViewModel对象时会通过ViewModelProvider的传递来绑定对应的声明周期。
  • ViewModel只有在Activity finish或者Fragment detach之后才会销毁。
image-20231003012052650

在Fragments间分享数据

有时候一个Activity中的两个或多个Fragment需要分享数据或者相互通信,这样就会带来很多问题,比如数据获取,相互确定生命周期。

使用ViewModel可以很好的解决这个问题。假设有这样两个Fragment,一个Fragment提供一个列表,另一个Fragment提供点击每个item现实的详细信息。

public class SharedViewModel extends ViewModel {private final MutableLiveData<Item> selected = new MutableLiveData<Item>();public void select(Item item) {selected.setValue(item);}public LiveData<Item> getSelected() {return selected;}
}public class MasterFragment extends Fragment {private SharedViewModel model;public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);//以上官方文档写法,已被弃用,采取以下写法:ViewModel viewModel = new ViewModelProvider(getActivity()/this).get(*ViewModel.class);itemSelector.setOnClickListener(item -> {model.select(item);});}
}public class DetailFragment extends LifecycleFragment {public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);//以上官方文档写法,已被弃用,采取以下写法:ViewModel viewModel = new ViewModelProvider(getActivity()/this).get(*ViewModel.class);ViewModel viewModel = new ViewModelProvider(getActivity()/this).get(*ViewModel.class);model.getSelected().observe(this, { item ->// update UI});}
}

两个Fragment都是通过getActivity()来获取ViewModelProvider。这意味着两个Activity都是获取的属于同一个Activity的同一个ShareViewModel实例。
这样做优点如下:

  • Activity不需要写任何额外的代码,也不需要关心Fragment之间的通信。
  • Fragment不需要处理除SharedViewModel以外其他的代码。这两个Fragment不需要知道对方是否存在。
  • Fragment的生命周期不会相互影响

ViewModel和SavedInstanceState对比

ViewModel使得在configuration change(旋转屏幕等)保存数据变的十分方便,但是这不能用于应用被系统杀死时持久化数据。举个简单的例子,有一个界面展示用户信息。
不应该把整个用户信息放到SavedInstanceState里,而是把单个用户对应的id放到SavedInstanceState,等到界面恢复时,再通过id去获取详细的信息。这些详细的信息应该被存放在数据库中。

image-20231003012655616

Jetpack ViewModel 并不等价于 MVVM ViewModel

这二者没有在同一个层次,MVVM ViewModel是MVVM架构中的一个角色,看不见摸不着只是一种思想。 而Jetpack ViewModel是一个实实在在的框架用于做状态托管,有对应的作用域可跟随Activity/Fragment生命周期,但这种特性恰好可以充当MVVM ViewModel的角色,分隔数据层和视图层并做数据托管。

所以结论是Jetpack ViewModel可以充当MVVM ViewModel 但二者并不等价

但jetpack的组件是可以分别使用的,所以可以先熟悉各个组件,然后往自己的项目里套。

ViewModel原理

val model = ViewModelProvider(this).get(TimerViewModel::class.java)

首先看ViewModelProvider的源码:

public class ViewModelProvider {private final Factory mFactory;private final ViewModelStore mViewModelStore;/*** Creates {@code ViewModelProvider}. This will create {@code ViewModels}* and retain them in a store of the given {@code ViewModelStoreOwner}.* <p>* This method will use the* {@link HasDefaultViewModelProviderFactory#getDefaultViewModelProviderFactory() default factory}* if the owner implements {@link HasDefaultViewModelProviderFactory}. Otherwise, a* {@link NewInstanceFactory} will be used.*/public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory(): NewInstanceFactory.getInstance());}//比较常用的构造函数public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {mFactory = factory;mViewModelStore = store;}public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {String canonicalName = modelClass.getCanonicalName();if (canonicalName == null) {throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");}return get(DEFAULT_KEY + ":" + canonicalName, modelClass);}//获得ViewModel实例public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {ViewModel viewModel = mViewModelStore.get(key);if (modelClass.isInstance(viewModel)) {if (mFactory instanceof OnRequeryFactory) {((OnRequeryFactory) mFactory).onRequery(viewModel);}return (T) viewModel;} else {//noinspection StatementWithEmptyBodyif (viewModel != null) {// TODO: log a warning.}}if (mFactory instanceof KeyedFactory) {viewModel = ((KeyedFactory) mFactory).create(key, modelClass);} else {viewModel = mFactory.create(modelClass);}mViewModelStore.put(key, viewModel);return (T) viewModel;}}    

ViewModelProvider接收一个ViewModelStoreOwner对象作为参数。在以上示例代码中该参数是this,指代当前的Activity。这是因为我们的Activity继承自ComponentActivity,而在androidx依赖包中,ComponentActivity默认实现了ViewModelStoreOwner接口:

public class ComponentActivity extends androidx.core.app.ComponentActivity implementsLifecycleOwner,ViewModelStoreOwner,SavedStateRegistryOwner,OnBackPressedDispatcherOwner {public ViewModelStore getViewModelStore() {if (getApplication() == null) {throw new IllegalStateException("Your activity is not yet attached to the "+ "Application instance. You can't request ViewModel before onCreate call.");}if (mViewModelStore == null) {NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();if (nc != null) {// Restore the ViewModelStore from NonConfigurationInstancesmViewModelStore = nc.viewModelStore;}if (mViewModelStore == null) {mViewModelStore = new ViewModelStore();}}return mViewModelStore;}            
}
public interface ViewModelStoreOwner {/*** Returns owned {@link ViewModelStore}** @return a {@code ViewModelStore}*/@NonNullViewModelStore getViewModelStore();
}

ViewModelStore类的代码是:

public class ViewModelStore {private final HashMap<String, ViewModel> mMap = new HashMap<>();final void put(String key, ViewModel viewModel) {ViewModel oldViewModel = mMap.put(key, viewModel);if (oldViewModel != null) {oldViewModel.onCleared();}}final ViewModel get(String key) {return mMap.get(key);}Set<String> keys() {return new HashSet<>(mMap.keySet());}/***  Clears internal storage and notifies ViewModels that they are no longer used.*/public final void clear() {for (ViewModel vm : mMap.values()) {vm.clear();}mMap.clear();}
}

从ViewModelStore的源码可以看出,ViewModel实际上是以HashMap<String,ViewModel>的形式被缓存起来了。ViewModel与页面之间没有直接的关联,它们通过ViewModelProvider进行关联。

当页面需要ViewModel时,会向ViewModelProvider索要,ViewModelProvider检查该ViewModel是否已经存在于缓存中,若存在,则直接返回,若不存在,则实例化一个

因此,Activity由于配置变化导致的销毁重建并不会影响ViewModel,ViewModel是独立于页面而存在的。也正因为此,我们在使用ViewModel时,需要特别注意,不要向ViewModel中传入任何类型的Context或带有Context引用的对象,这可能会导致页面无法被销毁,从而引发内存泄漏。

ViewModel与AndroidViewModel

在使用ViewModel时,不能将任何类型的Context或含有Context引用的对象传入ViewModel,因为这可能会导致内存泄漏。但如果你希望在ViewModel中使用Context,该怎么办呢?可以使用AndroidViewModel类,它继承自ViewModel,并接收Application作为Context。这意味着,AndroidViewModel的生命周期和Application是一样的,那么这就不算是一个内存泄漏了。

在上面的示例中,我们通过自定义接口OnTimeChangeListener来完成ViewModel与页面之间的通信。这不是一个好的方案。Jetpack为我们提供了LiveData组件来解决这个问题。通过LiveData,当ViewModel中的数据发生变化时,页面能自动收到通知,进而更新UI。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/154096.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Playcanvas后处理-辉光bloom

&#xff08;一&#xff09;Bloom介绍 Bloom&#xff08;辉光、光晕、泛光&#xff09;是一种常见的摄像机后处理&#xff08;PostProcessing&#xff09;效果&#xff0c;用于再现真实世界相机的成像伪影。这种效果会产生从图像中明亮区域边界延伸的光条纹&#xff08;或羽毛…

Altium Designer学习笔记2

原理图的绘制 需要掌握的是系统自带原理图库元件的添加。

[WUSTCTF 2020]level2 脱壳

这里我们发现ida打开后是有报错的 所以猜测是否有壳 所以我们去看看是否有壳 发现去壳了 然后再放到IDA中看即可

Atlassian发布最新补贴政策,Jira/Confluence迁移上云最低可至零成本

到2024年2月15日&#xff0c;Atlassian将不再提供对Jira、Confluence、Jira Service Management等Server版产品的支持。 近期&#xff0c;Atlassian推出了一项针对云产品的特殊优惠。现在从Server版迁移到云版&#xff0c;您能享受到高额补贴&#xff0c;甚至成本低至零元。立…

Odoo:行业领先的免费开源财务管理解决方案

面向现代企业的财务和会计软件 可靠关账&#xff0c;更快速、更准确地报告财务数据 Odoo ERP财务和会计软件可帮助财务主管设计、革新和理顺财务流程和运营。Odoo ERP无缝整合各种核心财务和会计功能&#xff0c;提供强大的合规管理特性&#xff0c;有助于企业改善业务绩效、提…

Selenium4+python被单独定义<div>的动态输入框和二级下拉框要怎么定位?

今天在做练习题的时候,发现几个问题捣鼓了好久,写下这篇来记录 问题一: 有层级的复选框无法定位到二级目录 对于这种拥有二级框的选项无法定位,也不是<select>属性. 我们查看下HTML,发现它是被单独封装在body内拥有动态属性的独立<div>,当窗口点击的时候才会触发…

十六、RabbitMQ快速入门

目录 一、在centos上下载MQ镜像 二、安装运行容器 三、登录进入MQ 1、添加一个新的用户 2、新建虚拟机 3、 为用户分配权限 四、RabbitMQ的基本概念 RabbitMQ中的几个概念: 五、常见消息模型 六、简单的消息生产与消费 1、消费者类 2、生产者类 3、基本消息队列的消…

数据库中的undo与redo的作用

undo与redo的作用 ​专栏内容&#xff1a; 手写数据库toadb 本专栏主要介绍如何从零开发&#xff0c;开发的步骤&#xff0c;以及开发过程中的涉及的原理&#xff0c;遇到的问题等&#xff0c;让大家能跟上并且可以一起开发&#xff0c;让每个需要的人成为参与者。 本专栏会定期…

Kubernetes Gateway API 攻略:解锁集群流量服务新维度!

Kubernetes Gateway API 刚刚 GA&#xff0c;旨在改进将集群服务暴露给外部的过程。这其中包括一套更标准、更强大的 API资源&#xff0c;用于管理已暴露的服务。在这篇文章中&#xff0c;我将介绍 Gateway API 资源&#xff0c;并以 Istio 为例来展示这些资源是如何关联的。通…

C语言之sizeof 和 strlen 详细介绍

C语言之sizeof 和 strlen 文章目录 C语言之sizeof 和 strlen1. sizeof 和 strlen 的比较1.1 sizeof1.2 strlen1.3 sizeof 和 strlen 的对比 2. 练习2.1.1 一维数组2.1.2 字符数组 1. sizeof 和 strlen 的比较 1.1 sizeof sizeof是C语言中的一个关键字&#xff0c;计算的是变量…

设计模式常见面试题

简单梳理下二十三种设计模式&#xff0c;在使用设计模式的时候&#xff0c;不仅要对其分类了然于胸&#xff0c;还要了解每个设计模式的应用场景、设计与实现&#xff0c;以及其优缺点。同时&#xff0c;还要能区分功能相近的设计模式&#xff0c;避免出现误用的情况。 什么是…

python -opencv形态学操作

python -opencv形态学操作 1.服饰和膨胀 1.服饰和膨胀 opencv 腐蚀通过cv2.erode实现&#xff0c;膨胀通过cv2.dilate实现&#xff0c;看一下下面代码&#xff1a; from ctypes.wintypes import SIZE from multiprocessing.pool import IMapUnorderedIterator import cv2 i…

76基于matlab的免疫算法求解配送中心选址问题,根据配送地址确定最佳配送中心地址位置。

基于matlab的免疫算法求解配送中心选址问题&#xff0c;根据配送地址确定最佳配送中心地址位置。数据可更换自己的&#xff0c;程序已调通&#xff0c;可直接运行。 76matlab免疫算法配送中心选址 (xiaohongshu.com)

泛型进阶:通配符

基本概念 对泛型不了解的可以看这篇博客&#xff1a;数据结构前瞻-CSDN博客 一般来说&#xff0c;&#xff1f;在泛型里的使用就是通配符 看看下面的代码 class Message<T> {private T message ;public T getMessage() {return message;}public void setMessage(T m…

网络协议头分析及抓包三次挥手四次握手

数据的封装与传递过程 思考&#xff1a; 应用层调用send后&#xff0c;是如何把数据发送到另一台机器的某个进程的。接收的设备收到数据包后&#xff0c;如何处理给应用层&#xff1f; MTU &#xff1a; Maximum Transmit Unit 最大传输单元 物理接口&#xff08;数据链路层&am…

Lightsail VPS 实例在哪些方面胜过 EC2 实例?

文章作者&#xff1a;Libai 引言 Lightsail VPS 实例和 EC2 实例是云计算领域中两种受欢迎的技术。虽然两者都提供虚拟服务器解决方案&#xff0c;但了解 Lightsail VPS 实例在哪些方面胜过 EC2 实例非常重要。在本文中&#xff0c;我们将探讨这两种技术之间的关键区别&#x…

CI/CD - jenkins

目录 一、部署 1、简介 2、部署 二、配置 三、实时触发 四、自动化构建docker镜像 五、通过ssh插件交付任务 六、添加jenkins节点 七、RBAC 八、pipeline 九、jenkins结合ansible参数化构建 1、安装ansible 2、新建gitlab项目 3、jenkins新建项目playbook 一、部…

choices参数的使用、MVC和MTV的模式、创建表对表关系的三种创建方式

【1】choices参数的使用 应用场景&#xff1a;针对表中可能列表完全的字段&#xff0c;采用choices参数 例如&#xff1a;性别&#xff0c;代码如下 # 1.创建一张表class gender_info(models.Model):name models.CharField(max_length32)password models.CharField(max_lengt…

大数据题目的解题技巧

目录 大数据题目的技巧总括 实例精析 实例一 实例二 实例三 大数据题目的技巧总括 &#xff08;1&#xff09;哈希函数可以把数据按照种类均匀分流&#xff1b; &#xff08;2&#xff09;布隆过滤器用于集合的建立与查询&#xff0c;并可以节省大量空间&#xff1b; &…

ubuntu 安装 gparted

前提环境&#xff1a; 阿里云的源。 sudo apt update sudo apt upgrade sudo apt install gparted 搜索&#xff1a;