ViewModel 源码设计思路分析

 

前言

转眼一年又过去大半了,在2022年,初定了大多计划,搬家,换公司,很多事情都一托再拖。

这里分享一篇我在公司内部做的分享文章吧,删除了部分对公司内部代码的探讨。

公司中的项目运用到了大量的组件封装。有的是对第三方组件进行二次封装,有的是从零开始设计,如何设计一个可扩展性高,容易使用的组件呢?可以参考参考google开发者是如何设计组件的。

我们以ViewModel组件为例,试图从源码中,窥探作者是如何设计ViewModel组件的。如果是我们,我们会怎么设计ViewModel组件呢?

需求分析

首先,需要明确我们的组件需要什么功能:

1、这个组件专门处理业务的数据逻辑

2、给Activity或者Fragment用的,希望同一个Activity下的fragment可以利用这个VeiwModel交互传递数据。

3、配置发生变化,导致View销毁重建的时候,保存这个ViewModel。数据不丢失。

4、ViewModel虽然不能持有Acitvity、fragment这种上下文,但是允许给一个Application。

5、进程意外杀死重建的时候,需要保存数据

头脑风暴(核心)

整理了需求之后,作者开始了头脑风暴,一起看看作者是如何思考的。

1、用于逻辑处理的类ViewModel。先搞个抽象父类,模板模式封装公共代码,逻辑交给子类。到时候用户继承我们的抽象类就好了。

2、保存这个功能要如何处理?我们希望在Activity或者Fragment中使用。并且到时候,一个Activity,可能有多个ViewModel。作者创建了一个新的类:ViewModelStore。专门用来保存一个界面的多个ViewModel。而VeiwModel负责处理业务逻辑,ViewModel的存储交给ViewModelStore.符合单一职责原则。

3、有了ViewModelStore保存我们的ViewModel了,那配置发生变化的时候,只需要存VeiwModelStore就好了。不是配置发生变化导致的界面销毁,就需要清空ViewModelStore中的ViewModel了。什么时候该保存,什么时候该清空,那就不是ViewModelStore这个类需要关心的事情了。因为ViewModelStore的责任只是用来保存ViewModel的。谁该关心呢?作者认为,谁持有,谁关心~作者创建了一个接口类ViewModelStoreOwner用来声明职责,给个方法,返回一个ViewModelStore,实现了这个接口,就会持有ViewModelStore了。那就由他去操心保存和清空ViewModelStore的功能吧。

可见,职责声明的场景,用接口来表示。

3.1、作者让ComponentActivity实现了接口,并在构造函数中监听生命周期回调。当生命周期走向onDestroy的时候,判断当前是不是因为配置变化导致的销毁Activity,不是的话,就清空ViewModelStore中的数据。

3.2、那具体怎么保存数据呢?当因为配置发生变化的时候,Acitvity的生命周期被安排走向尽头。在ActivityThread中,调用activity的retainNonConfigurationInstances。该方法返回一个NonConfigurationInstances对象(可以认为包装类),把对象用变量lastNonConfigutationInstances保存在ActivityCliendRecord中。在该方法中

NonConfigurationInstances retainNonConfigurationInstances() {
 //该方法是空实现,用于保存子类的对象
 Object activity = onRetainNonConfigurationInstance();

 //...忽略代码 

 NonConfigurationInstances nci = new NonConfigurationInstances();
 nci.activity = activity;

 //...忽略代码
 return nci;
 }

 

所以ComponentActivity实现onRetainNonConfigurationInstance并返回ViewModelStore对象即可。

不过,作者明白,只有一些刁民,喜欢瞎搞事情。

 为了防止这些刁民,作者在ComponentAcitvity中把这个onRetainNonConfigurationInstance方法写了final,防止他们重写了方法,覆盖了我们的代码。这个方法就用来给我们保存数据的.

 但是做人做事都不能赶尽杀绝啊,毕竟大部分的用户都是遵纪守法的好公民,这个方法只保存我们的ViewModelStore,也太局限了。于是,作者就创建了一个空函数,用户实现这个方法,返回他们想要正常保存的数据,到时候,连带帮用户的数据一起存起来不就好了。所以到时候补一个用户实现这个方法,配置发生变化的时候,顺带帮他们一起保存了。既然要存的有2种数据,一种是用户的,另一种是ViewModelStore。那就需要创建一个包装类了,把这俩包装起来。于是ComponentActivity中,就需要创建一个静态内部类了。

这种实现父类给的方法,但是不赶紧杀绝的做法很赞,在我们的项目中,也发现了类似的写法。此处省略一万字。。。。。

当Activity重新创建的时候,再来个方法返回我们之前保存的数据就好了~

在Activtiy的attach函数中,会把之前的配置传回来。

这样子类在就可以通过这个对象获取之前我们存储的ViewModelStore了。

如此一来,保存数据和获取数据的方案都有了~

到目前为止,我们已经有一个专门处理逻辑的类ViewModel,一个专门保存ViewModel的类ViewModelStore,以及利用接口ViewModelStoreOwner去声明相关职责了。保存清空的思路也都有了。

 4、下面就需要考虑如何去创建了

用户是可以自由定义ViewModel的具体的类的,只需要继承我们的ViewModel就好了,并且ViewModel的构造函数中,可能有用户自己的参数。也可能没有参数或者是有一个Application的参数。这种情况下,作者想到了利用工厂模式,为每一个ViewModel提供一个工厂类,如果有构造函数有自定义的参数,那就自己搞个工厂类去创建,没有参数或这有一个Application参数的时候,由我们提供的工厂创建就好了。

工厂类接口很简单:

 作者为没有参数的构造函数和参数只有一个Applicaiton的构造函数,创建一个默认的工厂类NewInstanceFactory和AndridViewModelFactory

 使用反射直接获取具体的类对象。带有Application参数的工厂类也是一样的,只是多了一个参数而已。问题不大:AndroidViewModelFactory

5、创建的方法已经有了。下面就是思考如何获取拿到ViewModel实例对象了。为什么要这么考虑呢?利用工厂模式不是已经可以获取到实例对象了?

因为我们有存储机制,当配置变化的时候,不再是通过factory获取了,而是从ViewModelStore中获取。

先整理一下获取ViewModel需要的相关逻辑:

(1)获取到对应的工厂实例对象

(2)从viewModelStore中获取ViewModel,或者利用工厂去创建一个ViewModel。

(3)创建之后,同时需要保存ViewModel。

这三部就是创建的流程需要涉及的相关逻辑

对于用户而言,他不应该关心如何保存我们的ViewMdel,什么时候从ViewModelStore中获取,什么时候创建,这些都应该是用户无感知的。这遵循迪米特原则:最少知道原则,用户在调用ViewModel组件的时候,不应该知道这个类的内部细节,以及内部调用逻辑。比如说外观者模式,或者中介者模式就符合迪米特原则。外观者,封装了组件内部实现细节,并提供统一的接口或者函数给外部。也降低了使用成本。这种最常见的就是Glide。简单的一行内码,就可以整合了内部千军万马的组件调用逻辑。

作者创建了一个新的类:ViewModelProvider。对外提供一个get函数,利用传入的具体的class,使用对应的工厂创建ViewModel。内部逻辑怎么调用,与用户无关。

这个ViewModelProvider做了什么事情呢?

1、根据不同的情况,拿到对应的工厂类。

2、从缓存中获取ViewModel,拿不到的时候,利用工厂类创建ViewModel。

3、把ViewModel保存到ViewModelStore中。

小结:创建的流程,使用了工厂模式和外观者模式,把创建的流程和业务代码隔离开,并且提供统一的对外访问函数get,降低用户使用成本。

5.1、具体看看这个ViewModelProvider是怎么做的,

ViewModelStore肯定是从外部(View层)带进来了,View层的ComponentActivtiy和Fragment都实现了ViewModelStoreOwner的接口,利用这个接口,就可以拿到了ViewModelStore了。另外还需要根据具体情况获取到默认的Factory。提供默认Factory这个职责,作者又创建一个接口类,HasDefaultViewModelProviderFactory,这个接口也只有一个方法,就是用来返回默认的Factory的。ComponentActivity去负责创建这个默认Factory在合适不过了。毕竟还要获取到application。这个接口类和Owner接口类搭配使用。

可以发现,对于每一个不同的功能职责声明,都有一个独立的接口去负责,并不会因为这些功能都和VeiwModel相关,也都用在一起,而把不同功能的接口整合在一起。就是典型的接口隔离的思想。

到这里,基本的逻辑已经完善了,创建了几个单一职责的类:ViewModel、ViewModelStore,外观者模式类:ViewModelProder、工厂模式涉及的具体的Factory。每一个类都有各自的功能。创建了声明职责的HasDefaultViewModelProviderFactory、ViewModelStoreOwner接口,遵循接口隔离原则。

继续看:

6、ViewModel只能保存因为配置发生变化导致界面重建的数据,如果因为系统资源限制等,导致回来的时候进程重新创建了,我们现有的逻辑是保存数据走onSaveInstanceState的方法。这个只能在View层处理了,这不就是解耦的不够彻底了。于是,就有一个新的类SaveStateHandle

保存这块数据,底层逻辑肯定还是需要调用OnSaveInstanceState,但是ViewModel和Activity是完全解耦的,如何交互呢?

作者想到使用第三者(一个新的类saveStateRegistry)来建立两者的沟通,这就有一些中介者味道了,比如我们非常熟悉的MVC架构,controller就是M和V的中介者。

通过第三者,在onCreate的时候,拿到数据,在onSaveInstanceState的时候,把数据存起来

 

 mSaveStateRegistry这个类和ViewModelStore一样,就是用来做数据存储的,从onCreate中获取数据,把数据用map保存,当需要调用onSaveInstanceState的时候,从外部拿到bundle,把我们的map都存到这个bundle中。

这个mSaveStateRegistry就像是一个model,提供数据的。当然,作者并不直接像上面这样,在onCreate中直接调用mSaveStateRegistry,在调用mSaveStateRegsitry.performStore方法得时候,增加了一些逻辑判断。而逻辑处理的逻辑不应该写在Activity中,更不应该写在数据存储类里面,作者使用了mvc模式,增加一个类作为控制层SaveStateRegistryController,负责处理SaveStateRegistry存储数据时候的额外逻辑。V是Activity,M就是SaveStateRegistry。最终:

 

 下面就是要思考如何把我们的ViewModel和这个SaveStateRegistry建立关系了。作者并没有直接让SaveStateRegistry持有ViewModel。这点和ViewModelStore不同。通过创建了一个接口类SavedStateProvider,这个接口的唯一方法就是返回一个bundle。SaveStateRegistry使用map存储这个接口。而ViewModel间接持有这个接口的实例对象。从而实现了ViewModel和SaveStateRegistry的解耦。

另外,对于ViewModel而言,也需要一个数据类,作者创建了一个新的数据类:SaveStateHandle。目的是通过该类存储我们希望进程意外杀死重建的时候的数据。同样的,SaveStateHandle是对数据的存储操作,该类就持有SaveStateProvider的接口实例对象。作者同样需要做一些额外的操作,比如把SaveStateHandle中的SaveStateProvider对象注册到SaveStateRegistry中,并监听生命周期,保证只注册一次。这些额外的逻辑操作同理交给SaveStatehandleController处理。

所以,ViewModel会持有SaveStateHandle,SaveStateHandle是数据类型,是Model,由控制类SaveStateHandleController提供,逻辑也有它负责。同样是MVC模式。

题外话:ViewModel和Activity的关系很淡泊,ViewModel持有SaveStateHandle对象,SaveStateHandle持有一个SaveStateProvider对象,这个对象被注册到SaveStateRegistry中,SaveStateRegistry不会直接和View层交互,而是通过SaveStateRegistryController。旁系三代以外就可以结婚了。这俩隔离这么远,可以说是妥妥的没啥联系了。

SaveStateHandle怎么和ViewModel建立联系呢?

现在只要在创建ViewModel的时候,把这个SaveStateHandler传给ViewModel即可。

如果是我的话,我会在ViewModel抽象父类中,创建这个成员变量。但作者的思路不同,他对构造函数进行解析,如果构造函数的参数只有SaveStateHandle这个类,或者是SaveStateHandle和Application的类型,在创建的时候,就把这个SaveStateHandle传输传入。这样用户就可以创建一个次构,来获取这个SaveStateHandle了。不过这种做法,有一个问题,如果是用户自定义的Factory,那要就获取不到了。所以在SaveStateHandle的注释中有提到:

 

如果想拿到这货,就需要使用SaveStateViewModelFactory了,不能用自己的。如果用全局变量,感觉就没有构造函数的限制了。

大致依赖关系

 

小彩蛋:

1、在SaveStateHandle中,这里key是直接写死的:

 获取的时候

 

 2、比如ViewModelStore:使用的都是hashmap,而不是ArrayMap。

 思考:是否在某些场景下,HashMap的性能会优于ArrayMap?因为如果HashMap一无是处,为什么没有被打上过期的标识?

 

3、外部访问权限控制

 这段代码源自NewInstanceFactory中,可以发现,专门创建了一个instance用来对外返回sInstance这个对象。而sInstance被声明称内部私有。pubclic修饰不可修改,很早之前google的一些VeiwModel中也遇到过。感觉意义不是很大。

最后

ViewModel组件设计思路差不多就到这了。本次技术分享主要是浅谈一下ViewModel组件的作者可能是如何思考并设计这个组件的。ViewModel的源码细节实际更多一些,可以发现该组件的设计也运用了大量的设计模式和设计思想,当我们自己设计组件的时候,有相似场景的时候。可以参考借鉴一下~。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
ViewModel是Mvvm设计模式中的一个重要组成部分,它的主要作用是将Model和View进行解耦,将业务逻辑从View中分离出来,使得View更加轻量级、易于测试和维护。 以下是ViewModel设计的一些常用技巧: 1. ViewModel应该只包含业务逻辑和数据,不应该包含任何UI相关的代码。这意味着ViewModel应该与具体的UI框架无关,可以在不同的平台上重用。 2. ViewModel应该暴露一些属性和命令,以便View可以绑定它们。这些属性和命令应该与View所需的数据和操作相对应。 3. ViewModel应该与Model进行交互,获取数据并对数据进行处理。这可以通过使用依赖注入来实现,将Model注入到ViewModel中。 4. ViewModel应该具有可测试性。这意味着ViewModel应该易于测试,可以使用单元测试框架进行测试。 5. ViewModel应该遵循单一职责原则,仅包含与其关联的业务逻辑和数据。这有助于保持ViewModel的简洁性和可维护性。 6. ViewModel应该与View进行双向绑定,以便View可以及时地更新UI状态。这可以通过使用数据绑定库来实现,例如Android中的Data Binding。 7. ViewModel应该使用异步任务来执行长时间运行的操作,以避免阻塞UI线程。这可以通过使用协程或RxJava等库来实现。 总之,设计一个好的ViewModel需要考虑到可测试性、可重用性、简洁性和可维护性等多个方面,以便在实际开发中能够提高开发效率和代码质量。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值