传世不朽ViewModel

11/20/2023

本章与「状态保存与SavedState」章有较强联系,建议阅读过后再浏览本章节。

# 1、ViewModel的来源——从状态保存谈起

# 1.1、SavedState并不是ViewModel的特性

在「状态保存与SavedStated」一章中,提到了ViewModelSavedState的关系,其中提到了SavedStateHandle的意义是用来解决ViewModel难以「访问组件入参」以及「保存状态」的两个难题。

但是我们回头看SavedStateHandle是如何被引入ViewModel的:

viewModel_01

可以看见,SavedStateHandle通过构造函数传入ViewModel,也就是说ViewModel在默认情况下,是没有「访问组件入参」以及「保存状态」2个功能的,那么ViewModel的对于状态保存的意义在哪里呢?

答案是:ViewModel可以在「配置更改导致的Activity重建」后仍然保存自身的实例。

换句话说,开发者使用ViewModel之后,无需担心再花费精力去处理配置更新导致的组件销毁问题,因为ViewModel并不会受到影响。

那么这个「配置更改导致的Activity重建」后仍可以保存实例的机制又是如何实现的呢?

# 1.2、无人问津的onRetainNonConfigurationInstance()

Activity的源码中,存在着一个几乎没什么人用的Api——onRetainNonConfigurationInstance()

这个Api并没有在「状态保存与SavedStated」一章中被介绍的原因也是几乎没人使用它。

viewModel_02

从名字可以看出,这个方法的用途是保存一些与配置无关的实例,读过「状态保存与SavedStated」的读者肯定会联想到Activity中保存实例的方法:onSaveInstanceState(Bundle),两者的区别如下:

onSaveInstanceState(Bundle) onRetainNonConfigurationInstance()
调用时机 组件onStop()前/后被调用 配置发生改变时被调用
支持类型 只支持基础类型和Parcelable/Serializable类型 支持任意类型
大小限制 受Binder限制,数据不能超过1MB 大小不受限制
实现原理 先通过Binder反序列化,再存储于内存中 直接存于内存中

回到本节的标题,为什么onRetainNonConfigurationInstance()无论是支持的类型还是大小都遥遥领先于onSaveInstanceState(Bundle),但它却几乎不受程序员的待见呢?

答案就是它的调用时机过于局限了,这也和这个API的设计初衷有关系,因为它只能用于处理「配置更新导致的Activity销毁」的这种场景,因此它并不是每次都进入onStop()(高版本在之前,低版本在之后)都被调用。

对于配置发生改变时要保存的状态,onSaveInstanceState(Bundle)也能做,即使有类型和大小的限制,程序员们也习惯于统一往onSaveInstanceState(Bundle)中实现所有的状态保存逻辑,因为这能降低维护的复杂性。

以上造就了onRetainNonConfigurationInstance()几乎无人使用的窘境。

# 1.3、丢掉or进一步扩展,这是一个问题

上一节提到,onRetainNonConfigurationInstance()遭遇了程序员的冷落,但是这能证明这个Api是无用的吗?

如果你对Binder的机制有一点了解的话,可以知道的是,为了实现跨进程,所有通过Binder传输的对象都要反复的序列化和反序列化,这就导致了性能上的劣势,当然还存在大小的限制。

如果配置更新导致了组件的销毁,页面中其实存在着有许多不需要跟随配置更改而改变的字段,例如已经加载好的bitmap。对于这类内存巨大的字段用Binder存起来也不合适,这就给onRetainNonConfigurationInstance()这个Api生存的空间,开发者可以通过这个Api缓存一些较大的对象来避免因配置更改后反复加载的缺点。

但是又回到了上一节的问题,这个Api实在不好用,我们应该直接抛弃它吗?答案是否定的。

在1.1节中笔者提到的ViewModel了一个重要特性:「配置更新后不会销毁」,读者是否觉得它与onRetainNonConfigurationInstance()这个Api的特性非常相近呢?对的你没猜错,ViewModel就是基于这个Api来实现其跨越配置更改的特性的。

总结:onRetainNonConfigurationInstance()并不是没用了,而是谷歌基于这个Api实现了ViewModel,开发者只需要使用ViewModel便享受到了这个Api的便利。相对于难以使用的原生Api,ViewModel确实好用特别多。

注意一点的是:「不会因配置更新而销毁」并不是ViewModel的全部意义,这个只是它的一个非常重要的特性,ViewModel还有许多优秀的特性这点下面会聊到。

# 1.4、ViewModel如何跨越配置更新的鸿沟

经过前三节的铺垫,笔者想必已经明白了ViewModel是使用onRetainNonConfigurationInstance()来实现避免配置更新导致自身销毁的机制的,具体如何实现本节展开讲讲:

关于ComponentActivity,这个Activity的子类在「Lifecycle」与「状态保存」的章节中都频繁出现过,其基本是Jetpac核心功能的基础实现,因此下面的源码也是基于这个子类来讲解。

我们从ComponentActivity的源码出发,看看核心的代码:

viewModel_03

核心代码就一个方法,就是上文提到的onReainNonConfigurationInstance(),而且谷歌还让其标记为final,即不可继承重写,这个方法只会强制返回一个类:NonConfigurationInstances,其中包括了一个Object类型的custom,一个ViewModelStore

标记为final并不意味着开发者不能实现该方法原本非常灵活的任意类型的返回值,而是谷歌将其放在了NonConfigurationInstances这个类的custom中,重写onRetainCustomNonConfigutaionInstance()即可,不过这并不是重点(因为这只是一种兼容老开发模式的手段),该类另外一个成员变量ViewModelStore才是本篇文章的核心。

上文提到,NonConfigurationInstances的核心是ViewModelStore,因此我们可以去掉custom之后单独围绕它来看,那么这个Api就会被简化成下面这样:

viewModel_04

简化后的代码非常清晰,其实就是在保存ViewModelStore

也许你并不清楚什么ViewModelStoreViewModel的关系,这里你只需要明白一点即可:ViewModelStore是一个缓存ViewModel的容器,通过它就可以拿到ViewModel

在配置更新时保存ViewModelStore,并在组件重建之后重新拿到ViewModelStore,那么自然而然的就拿到了对应的ViewModel

我们看看ComponentActivity是如何拿到ViewModelStore的:

viewModel_05

每次ComponentActivity要访问ViewModelStore的时候,都会主动调用ensureViewModelStore()这个方法,看看有没有往非配置实例中写入ViewModelStore,如果有则读出来,如果没有就新建一个。

以上就是:「ViewModel可以在配置更新后不会销毁」的秘密。

# 2、ViewModel是谁?

ViewModel的定义放在第二节是笔者有意为之,在第一节中,笔者为读者展示了ViewModel如何解决了开发者一个巨大的痛点,即「处理因配置更新导致的组件销毁从而导致的状态丢失」的问题,读者相比已经对ViewModel有了一个基础的认知,但如同上面提到过得一样,跨越配置更新的鸿沟并不是ViewModel的全部优点和特性,下面为你逐步掀开ViewModel的头盖骨面纱。

# 2.1、定义

ViewModel 类是一种业务逻辑或屏幕级状态容器。它用于将状态公开给界面,以及封装相关的业务逻辑。它的主要优点是,它可以缓存状态,并可在配置更改后持久保留相应状态。这意味着在 activity 之间导航时或进行配置更改后(例如旋转屏幕时),界面将无需重新提取数据。

上文中重点讲了ViewModel可以让在配置更改后保存自身的存在,从定义中我们可以看出,其实ViewModel更重要的一个身份是「状态容器」,换句话说ViewModel负责广播状态,而组件(ActivityFragment)则回归纯粹UI的本质,引入ViewModel之后的不仅是多了一个组件的区别,更多的改变则在于开发范式的转变(关于这个话题下文会讲)。

# 2.2、ViewModel简单使用

简单看一下引入ViewModel之后的Activity的变化(Fragment代码基本类似,不重复展示):

viewModel_06

还记得ViewModel的两个特性吗,这里重温下:

  • 状态容器
  • 规避配置更新导致的丢失

图中一个显著的特征是原本应该位于Activity中的成员变量被移动到了ViewModel的内部,这体现了ViewModel作为状态容器的特性,这样做的好处就是让逻辑收拢在了ViewModel的内部,这让UI更加容易迁移和调试(因为降低了耦合度)。

「规避配置更新导致的丢失」这个特性在图中不好展示,但是读者可以参考上述的代码自己实现一个小demo,然后旋转屏幕,观察重建的Activity中的数据有没有发生丢失现象。

上文提到的「开发范式的转变」指的是开发模式逐渐过渡到MVVM或者其他开发思想中来,一种常见的特征就是状态均以LiveData或者StateFlow的形式出现。

# 2.3、SavedState的使用

关于SavedState的使用在「状态保存与SavedState」一章中有详细讲解如何使用,本章不再继续展开

# 3、ViewModel剖析

为了让读者对ViewModel的整个体系有大致的理解,这里先把ViewModel的几个关键组件列举出来,只需要留个印象即可,后续会串联起来。

# 3.1、核心组件

# 3.1.1、ViewModel

ViewModel无疑是该库中最核心的组件,但是其内部却极其简单,只有两个容器,主要的作用就是存放Tag和Closeable,这两者会在ViewModel被关闭的时候被清空。

viewModel_07

那么ViewModel何时被关闭呢?答案是组件遇到「非配置更新导致的销毁」的时候。

简单看看ComponentActivity的源码可以看见,只有非配置更新导致的销毁,才会让ViewModelStore销毁。

viewModel_22

综合来说,ViewModel的基类只提供一个销毁时的监听的功能,其创建、销毁由库中的其他组件实现,其业务实现则全部交给开发者。

# 3.1.2、ViewModelStore、ViewModelStoreOwner

ViewModelStore是存放ViewModel的仓库类,通过Key来区分不同的ViewModel实例。

ViewModelStoreOwner和生命周期篇讲过的LifecycleOwner基本类似的设计模式,本质只是一个提供实例的接口。

viewModel_08

ViewModelStore遵循以下原则:

  • ViewModelStore需要配置更改后仍然得到保留,如果没法保留而被销毁了,那么保存的ViewModel实例也要一样,这里简单来说就是map内部的成员实例都不能丢。
  • 如果ViewModelStore的持有者不再需要它而且也不会重新创建它,则其所有者需要调用ViewModelStore的clear()方法通知它不再使用。

以上两条原则虽然对于绝大多数开发者来说并不会使用,因为大部分开发者都不会亲自开发ViewModelStore,但是谷歌亲自开发的Jetpack库中,均遵循这两条准则,因此开发者将其理解为ViewModelStore的「特性」即可。

# 3.1.3、ViewModelProvider、Factory

上文提到ViewModelStore只是一个存储ViewModel的容器,它并没有创建ViewModel的功能,而ViewModelProvider正好弥补了这个功能。

从下图中可以看出两点:

  • ViewModelProvider通过工厂类创建ViewModel

  • ViewModelProvider的核心代码是get(),其原理就是简单的有就取缓存,没有就用工厂类创建一个ViewModel并放置在ViewModel与缓存中

viewModel_09

# 3.1.4、Factory

本小节由CreationExtras 来了,创建 ViewModel 的新方式 - 掘金 (juejin.cn) (opens new window)中精炼总结得来,可以读原文获取更加详细的信息。

上文提到 ViewModel 是使用工厂类来实例化的,因为ViewModelFragment需要在非开发者干预的情况下由系统创建,而工厂类就是定义了不同构造函数的创建方式。

viewModel_14

可以看出存在着两套创建方式,一套是带 CreationExtras一套是不带的,导致这种原因是ViewModel在2.5.0 版本中新增了一种带 CreationExtras 的构建方式。

要讲清楚这一套新增的方式做了什么,我们首先把目光放回以前,看看以前的方式:

# 3.1.4.1、传统的Factory与其缺陷

假设开发者需要在ViewModel中增加非空构造函数,则需要实现对应的工厂类,假设有一个ViewModel的构造函数需要Application和一个仓储类作为入参,则需要实现一个这样的工厂类:

viewModel_15

可以看出,该ViewModel的工厂类和其需要的参数基本要保持一致,这样才可以保证工厂类创建ViewModel时拥有所有的参数。

但是我们回顾这种方式有什么缺陷呢?其实缺陷还是挺大的,如下:

  • ViewModel需要的参数可能较多,而且不同的ViewModel也大相径庭,让工厂类丢失了工厂的作用,开发者几乎要为不同的ViewModel都创建一个工厂类。
  • 工厂类持有状态,不利于复用。

正因为上述的问题,谷歌提出了一种新的类似于Bundle的解决方式,就是上文提到的 CreationExtras

# 3.1.4.2、引入CreationExtras的巨变

我们重新看工厂类的另外一种方法,传入了一个CreationExtras的参数,这为新的构建方式提供了基础。

viewModel_14

CreationExtras就像Activity跳跃时的Intent传参一样,是通过key-value的方式传递的,因此比较灵活,看看使用了新版之后该如何构建ViewModel

viewModel_16

这个时候,Factory不再需要构造函数和任何成员变量了,变成了彻底的「无状态」,更利于复用。

可以看到的是,图中定义了一个key,是传输开发者定义的仓储类的,但是Application使用了一个预定义的Key,这个是谷歌开发者提前定义好的,其他还有几个预定义的key,如下图:

CreationExtras.Key Descriptions
ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY ViewModelProvider 可以基于 key 区分多个 VM 实例,VIEW_MODEL_KEY 用来提供当前 VM 的这个 key
ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY 提供当前 Application context
SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY 提供创建 createSavedStateHandle 所需的 SavedStateRegistryOwner
SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY createSavedStateHandle 所需的 ViewModelStoreOwner
SavedStateHandleSupport.DEFAULT_ARGS_KEY createSavedStateHandle 所需的 Bundle

上图展示的是工厂类如何使用该变量,但是变量是哪里传入的呢,读者还记得工厂类是哪里被使用吗?它是被ViewModelProvider使用的,那么CreateExtras自然是ViewModelProvider所提供的,我们重新看看下图:

viewModel_17

可以看到,ViewModelProvider在构建ViewModel的时候,会将默认参数传给工厂类,这样工厂类就可以在构建ViewModel的时候,通过对应的key拿到对应的value,这样就可以满足不同ViewModel的构建需要了。

# 3.1.5、小总结

下面以一张图总结各个组件之间的关系:

viewModel_10

# 3.2、ViewModel回首掏

# 3.2.1、从使用流程回首ViewModel

上文简单讲解了ViewModel几个核心组件的功能,下文将从ViewModel的使用流程去将几个组件所处的位置理清楚。

需要注意的是,由于需要讲解组件的使用,因此不会使用委托的方式创建ViewModel,同时以Activity为讲解组件。

下面看看ComponentActivity中使用ViewModel的典型案例:

viewModel_11

当生命周期进入onCreate()的时候,通过ViewModelProvider来新建一个MyViewModel,留意到工厂类传的值是defaultViewModelProviderFactory

这个参数来自于一个接口:HasDefaultViewModelProviderFactory

viewModel_12

这个接口是什么意思呢,这个接口是给ViewModelStoreOwner实现的,相当于某个StoreOwner存在着默认的创建ViewModel的工厂类。

上文中提到,ViewModelProvider需要从Owner手里获取工厂类来了解如何构建ViewModel,对于单个Owner来说,绝大多数情况创建ViewModel的方式都是相同的(大多数情况都是无参或者带SavedStateHandle),因此拥有一个「默认工厂」是极大的便利。

开发者可以在ComponentActivity中使用defaultViewModelProviderFactory的原因恰恰是它也实现了该接口:

viewModel_13

可以看出,这是一个创建「参数带有SavedState」的ViewModel的工厂类,同时SavedState默认带有Activity的getIntent().getExtras(),而在Fragment中则是getArugments()(图中没体现,读者可以去Fragment的源码中查看)。

因此开发者在ComponentActivity中创建ViewModelProvider的时候使用的默认工厂其实就是SavedStateViewModelFactory

如果你从来没有手动传过这个默认工厂也没关系,ViweModelProvider 会自动从ViewModelStoreOwner中拿,哪怕 ViewModelStoreOwner没有默认工厂也会使用一个最简单的实例化工厂,但这个工厂只能用于简单的无参实例化, 不过鉴于开发者普遍使用 ComponentActivity这个问题应该不存在。

# 3.2.1、重谈ViewModel的by委托

开发者目前比较喜欢的创建方式是使用by委托来创建ViewModel,如下:

viewModel_18

这种方式背后做了什么呢?我们看看viewModels的定义:

viewModel_19

可以看出,这个委托还有2个参数,分别是传入CreationExtrasFactory,读者还记得它们是做什么的吗?如果你忘了建议回去看看第三节。

继续从代码中可以读出,如果开发者不传Factory,那么就用使用ComponentActivity的默认工厂类,这个上面也提到了,实际上就是SavedStateViewModelFactory

相同的是,如果开发者不传CreationExtras,那么就会使用使用defaultViewModelCreationExtras

ViewModelLazy就不进一步研究了,代码非常简单,读者自行研究即可,本质上就是套了一层kotlin的lazy委托来实现懒加载。

终上所述,在ComponentActivity中使用by viewModels创建ViewModel基本等价于下面的代码:

viewModel_20

当然,使用by委托还可以享受懒加载的优势,这样就不用监听生命周期,建议一律使用谷歌官方封装的委托。

# 4、项目实践中看ViewModel

# 4.1、构建ViewModel的几种情况

# 4.1.1、构造函数为空的ViewModel

对于这种ViewModel使用来说最为简单,直接使用委托即可

# 4.1.2、需要访问SavedStateHandle的ViewModel

对于这种ViewModel使用也非常简单,直接使用委托即可,因为ComponentActivityFragment等常见场景都已经适配了SavedStateHandle

# 4.1.3、需要更多参数的ViewModel

# 4.1.3.1、自定义工厂类+自定义CreationExtras

对于这种场景,开发者需要在委托中传入自定义的工厂类,例如某个ViewModel需要一个SavedStateHandle和一个仓储类作为入参,开发者可以实现如下代码:

代码虽然多了很多,但是实际上只多做了几件事:

  • 自定义了一个key,在构建CreationExtra的时候,使用了该key传入用户自定义的仓储类。需要注意的是,由于CreationExtras不可变的设计,开发者需要用MutableCreationExtras套住原来的Extras来传递新值。

  • 构建工厂类的时候,通过用户自定义的key获取到了仓储类。需要注意的是获取SavedStateHandle并不是简单的使用key来获取,而是使用了谷歌官方封装的扩展方法,因为SavedStateHandle的构建相对麻烦,还需要访问SavedStateRegistry(状态保存一章有提到)。

viewModel_21
# 4.1.3.2、使用Hilt依赖注入框架实现参数注入

上述提到的工厂类仍然非常麻烦,虽然开发者只需要设计一个工厂类,但是对于不同的ViewModel仍需要实现不同的CreateExtras取值方式,有没有一种更解耦的方式呢,答案是有的,就是依赖注入。这里推荐使用Jetpack的Hilt。

关于Hilt的具体使用读者可以去安卓开发者文档参考具体使用说明,文章链接如下:

使用 Hilt 实现依赖项注入 (opens new window)

将 Hilt 和其他 Jetpack 库一起使用) (opens new window)

# 4.2、跨组件通信

在安卓中跨组件通信,特别是Activity与它治下的Fragment之间的消息通信以及这些Fragment之间相互通信一直是一个老大难的问题,因为这涉及了组件引用、生命周期等需要注意的因素:

  • 如果某个Fragment需要父组件通信,直接获取父组件引用会提高耦合度。
  • 如果多个Fragment需要共享同一个变量,变量应该缓存在哪里呢?如果放在父组件中依然会存在耦合度过高的问题(因为依然要访问父组件的真实引用)。
viewModel_23

引入了ViewModel的开发阶段之后,组件的实际逻辑已经迁移到ViewModel之中,因此直接让组件通过成员变量去沟通信息也不合适(这样也有配置更新导致的状态丢失问题)。

那获取其他组件的ViewModel不就行了吗?

答案是对的,既然逻辑已经迁移到ViewModel中了,同时ViewModel也有降低耦合性(避免直接拿组件的实例)、避免配置更新导致销毁等好处,那直接获取组件的ViewModel来实现信息的沟通是非常方便的。

那么问题就来到了「如何拿到对应组件的ViewModel」?

还记得开发者是如何在Activity或者Fragment中拿到ViewModel的吗,如果你已经忘记了可以重新阅读一下上文,开发者是通过ViewModelStore来获取ViewModel的,换句话说,「只要拿到ViewModelStore,就可以通过ViewModel的类来找到对应的ViewModel」。

因此开发者可以通过一下开发模式来实现Fragment之前的信息沟通:

  1. 把需要共享的信息以LiveData、StateFlow等不同形式(根据需要)声明在父组件的ViewModel中。
  2. Fragment在构建的时候通过父组件的ViewModelStore拿到父组件的ViewModel。
  3. Fragment通过修改父组件的ViewModel中的数据,通知订阅了该数据的其他Fragment,以达到Fragment之间通信的目的。

如下图所示:

viewModel_24

这样避免了以下缺点:

  • 避免子组件直接获取父组件引用(引用的是ViewModel)导致耦合度过高。
  • 避免子组件相互引用(通过父组件ViewModel间接通讯)。

那么开发者如何使用代码来落实「Fragment获取ActivityViewModel」呢?谷歌已经封装好了一个委托方法,叫activityViewModels(),和viewModels()委托基本没什么区别:

viewModel_25

读者可以看activityViewModels()的定义,其实并没有什么特别的,就是利用父ActivityViewModelStore来获取/创建一个的ViewModel,如果你理解了上文的内容,那么这个方法对你来说应该没有什么别的困难的。

通过这种方式,Activity下面的所有Fragment都可以拿到同一个ViewModelStore,因此也可以拿到同一个ViewModel

假如开发者想获取Fragment的父FragmentViewModel呢?

虽然谷歌没有提供直接的扩展方法(可能是觉得没什么用),但是可以模仿获取ActivityViewModel的方式,写一个获取父Fragment的版本:

viewModel_26

思路是一样的,围绕着ViewModelStore理解即可,这是ViewModel的根源。

# 5、总结

ViewModel是安卓开发进入MVVM时代的产物,后MVVM时代不仅是开发模式与开发思想都发生了剧烈的变化,作为漩涡中心的ViewModel的位置的重要性不可小视,然而ViewModel的却是许多开发中眼中的「最熟悉的陌生人」,多数开发者只会使用或者做少许的定制,对ViewModel的实现机制并不了解,这限制了开发者的能力上限甚至写出了有问题的ViewModel代码。

本文从ViewModel能够跨越配置更新出发,讲解了ViewModel的核心组件的原理与关系,并提供了一些基础的定制化方案,但仍有许多细节和定制化内容有待读者挖掘。