ViewModel 是 Jetpack 中非常重要的一个库,它与 Lifecycle、LiveData 构建了 MAD 的三大基础组件。在 ViewModel 之前,Activity 负责处理所有内容,这使得其变得极为庞大,并且需要负责由于其混乱的生命周期带来的一系列问题。因此,ViewModel 在推出之后就以摧枯拉朽之势席卷了整个 Android 开发界。
本文将带你走进 ViewModel,看看它的前世今生,以及使用方法。阅读此文,你必将对 ViewModel 有一个新的认识。不过为了说明 ViewModel 的作用,我们需要回到 Jetpack 之前那些没有 ViewModel 的日子里。
在 ViewModel 之前
在 ViewModel 出现之前,Android 开发中 Activity 既要管 UI 显示,又要管数据获取和处理。在这时,一个典型的登录页 Activity 大概率会是这个样子:
classLoginActivity:Activity(){privatelateinitvaretUsername:EditTextprivatelateinitvaretPassword:EditTextprivatelateinitvarprogressBar:ProgressBaroverridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_login)etUsername=findViewById(R.id.et_username)etPassword=findViewById(R.id.et_password)progressBar=findViewById(R.id.progress)}funonLoginClick(view:View){valusername=etUsername.text.toString()valpassword=etPassword.text.toString()progressBar.visibility=View.VISIBLE// 直接在 Activity 里发起网络请求Thread{valsuccess=loginApi.login(username,password)runOnUiThread{progressBar.visibility=View.GONEif(success){startActivity(Intent(this,MainActivity::class.java))finish()}else{Toast.makeText(this,"登录失败",Toast.LENGTH_SHORT).show()}}}.start()}}这样的 Activity 在早期是非常常见的,获取UI组件,调用接口,设置回调结果等等。看起来能跑,但是这里有三个致命问题:
问题一:只要配置发生变更,数据将全部丢失
在 Android 系统中,配置变更(屏幕旋转、语言切换、键盘弹出)会销毁并重建 Activity,而这里要格外注意,重建的 Activity 是一个全新的 Activity,与之前的没有半点关系。
用户竖屏输入完账号密码,网络请求正在飞行中,转了一下屏幕——Activity 销毁重建,那个网络请求的回调还持有旧 Activity 的引用,新 Activity 完全不知道有这个请求在跑。
更常见的情况:加载了一个列表数据,转屏后重新加载一遍。用户流量白花了,体验也差。
面对这个问题,早期开发者都会通过onSaveInstanceState(outState: Bundle)和onRestoreInstanceState(savedInstanceState: Bundle)来进行处理,虽然麻烦,但是能解决因为配置变更导致 Activity 重建问题。
问题二:异步回调持有已销毁的 Activity
这也是 Activity 重建导致的问题,例如上面的典型的 Activity 的例子中的网络请求,如果 Activity 发生重建,这种代码就是非常有问题的。
// Activity 已经被销毁,但回调还在持有 thisThread{valresult=api.fetchData()runOnUiThread{// Activity 可能已经 finish 了textView.text=result.data// crash 或内存泄漏}}.start()当网络请求、数据库查询这些异步操作的生命周期,跟 Activity 的生命周期不对齐时,往往就会伴随各种异常,而且通常都是非必现的运行时问题。
面对这个问题,开发者需要在回调中使用isFinishing()、isDestroyed()这些方法来检查状态,但是你得在每个异步回调里手动检查生命周期状态,代码里到处散落if (isFinishing() || isDestroyed()) return;,既丑又容易遗漏。
问题三:Activity 膨胀到难以维护
上面两个问题,都是由 Activity 生命周期带来的问题,虽然麻烦,但是这两个问题是有解的。但是伴随这些问题的解决,你的 Activity 也会迅速膨胀,特别是当业务逻辑稍复杂时,Activity 会膨胀成几千行——网络请求、数据缓存、UI 更新、状态管理全堆在一起。于是工程上的复杂度将增加到无法维护的地步:
- 难以测试:所有逻辑都绑定在 Android 框架对象上
- 难以维护:改一个功能要在上千行代码里翻找
- 难以复用:数据和 UI 耦合在一起,换一个界面就要重写
Google 的解法
Google 当然也知道混乱的 Activity 生命周期是一个极为糟糕的设计,但是木已成舟,无法更改,只能不断地往 Android 系统这艘破船上添加木板来解决这个问题。
在软件领域,没有添加一个中间层不能解决的问题,如果有,就添加两个。
而 Google 添加的中间层,就是 ViewModel。Google 的想法很简单,核心就一件事:把数据和 UI 分开,让数据在配置变更中活下来。
将数据独立出来之后,上面的三个问题也就都能解决了:
- 配置变更数据丢失:ViewModel 的生命周期比 Activity 长,配置变更时不会被销毁,数据自动保留
- 异步回调引用已死 Activity:ViewModel 持有数据,Activity 重建后重新观察 ViewModel,不存在"回调引用旧 Activity"的问题
- Activity 巨大不可测试:业务逻辑搬到 ViewModel,ViewModel 不持有 Android 框架类(View、Activity),可以直接写单元测试
这里需要注意的一点是,Activity 通过观察 ViewModel 中的数据来展示界面,但 ViewModel 仅持有数据,因此为了观察数据,使用 ViewModel 往往需要搭配 LiveData 或是 Flow 来使用,而为了保证对 Activity 生命周期的正确处理,还需要依赖 Lifecycle 库。对这两个库不了解的,可以看一下以下两篇文章:
Jetpack 生命周期组件 Lifecycle 的设计思想和使用
Jetpack 可观察数据容器 LiveData 的入门与基础使用
为什么叫做 ViewModel
ViewModel 这个名字不是 Google 发明的,它来自微软的 MVVM 模式(Model-View-ViewModel),2005 年由微软的 John Gossman 在 WPF 中提出。Google 做的事情是:把这个已有的架构概念,用 Jetpack 库落到了 Android 上。
如果你想对 GUI 软件架构进一步了解,可以看这篇文章:从历史的角度看 Android 软件架构
在这个库中,ViewModel 中的 Model,不是指"数据存储",而是指对 UI 所需状态的建模。你可以理解 ViewModel 是:一个为 View 服务的 Model。而这个 Model,如果要翻译的话,需要翻译为“模型”,千万不能理解为 MVP 架构中特指的数据层。
一个对 ViewModel 的片面认知是它只是 Activity 中的数据的容器,但其实 ViewModel 里面装的不只是数据,还有:
- UI 状态的管理(loading、error、success 各种状态切换)
- 业务逻辑的调度(调 Repository 做网络请求)
- 跨配置变更的状态保持(这一点正好是 Activity 做不到的)
这些综合在一起,构成的是一个"为 View 服务的模型",而不仅仅是一个"数据仓库"。
ViewModel 的原则
ViewModel 的核心设计原则之一:绝不持有 View 层的引用(Activity、Fragment、View、Context)。
因为 Activity 会死,而 ViewModel 不会。如果 ViewModel 持有了 Activity 的引用,屏幕旋转时旧 Activity 被销毁,ViewModel 还持有旧 Activity 的引用——调用旧 Activity 的方法会 crash,旧 Activity 无法被 GC 回收会内存泄漏。
ViewModel 和 View 的沟通方式是观察者模式——ViewModel 暴露数据流,View 主动观察。因此,在 ViewModel 中的数据往往是 LiveData、Flow 这种类型。外部谁需要,谁观察。
ViewModel 的使用
前面我们说到了 ViewModel 的由来,现在我们一步一步在项目中加入 ViewModel,看看 ViewModel 如何改变我们的软件架构。
引入依赖
在 libs.versions.toml 中声明依赖:
[versions] lifecycle = "2.11.0" activity = "1.13.0" [libraries] androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-savedstate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } # 我们需要 ComponentActivity在 module 中的 build.gradle.kts 引入依赖:
dependencies{implementation(libs.androidx.activity.ktx)implementation(libs.androidx.lifecycle.viewmodel.ktx)implementation(libs.androidx.lifecycle.viewmodel.savedstate)}基本用法
创建 ViewModel
classFirstViewModel:ViewModel(){valtextLiveData=MutableLiveData<String>()overridefunonCleared(){super.onCleared()// Activity 真正退出时(非配置变更)自动调用}}在 Activity 中获取
classMainActivity:ComponentActivity(){privatelateinitvarviewModel:FirstViewModeloverridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_second)textView=findViewById(R.id.text_view)viewModel=ViewModelProvider(this)[FirstViewModel::class.java]viewModel.textLiveData.observe(this){textView.text=it}}}这里需要注意的是,我们创建 viewModel 使用的是ViewModelProvider,它只是一个工具类,我们使用它创建 ViewModel,不需要持有其引用。而其会调用this(这里是 ComponentActivity)的默认 Factory,用于创建 ViewModel。所以这里创建 ViewModel 的代码很简单。
一旦持有了一个 ViewModel 后,就可以观察 ViewModel 中的 LiveData 来同步到界面上了。
带构造参数的 ViewModel
默认 Factory 只能调用无参构造函数。如果 ViewModel 有构造参数,需要自定义 Factory。例如下面的 ViewModel:
classSecondViewModel(privatevalinitC:Int,valinitS:String,privatevalsavedStateHandle:SavedStateHandle):ViewModel(){varcounter:Intget()=savedStateHandle["counter"]?:initCset(value){savedStateHandle["counter"]=value}}为了创建这样的 ViewModel,我们必须提供 Factory 以供系统调用,毕竟这种带参数的 ViewModel,只有我们知道如何创建。
创建这个 ViewModel 的代码如下:
viewModel=ViewModelProvider(this,viewModelFactory{initializer{valsavedStateHandle=createSavedStateHandle()SecondViewModel(300,"Hello",savedStateHandle)}})[SecondViewModel::class.java]带 Application 的 ViewModel
前面创建的都是没有带 Context 的 ViewModel,但是如果 ViewModel 需要 Context 怎么办?面对这个问题,Google 提供了 AndroidViewModel——就是 ViewModel 的一个子类,多持有一个 Application 引用:
classMyAndroidViewModel(application:Application):AndroidViewModel(application){fundoSomething(){valcontext=getApplication<Application>()// ...}}注意这里只能用 Application 的 Context,不能用 Activity 的 Context。原因就是 ViewModel 不能持有 Activity 引用,否则配置变更时旧 Activity 被销毁,ViewModel 还持有它的引用,导致内存泄漏。
使用扩展方法 viewModels() 进行创建
创建 ViewModel 不只是可以像上面那样使用 ViewModelProvider 来创建,还可以通过 ComponentActivity 的扩展方法viewModels来创建:
classMainActivity:ComponentActivity(){privatevalfirstViewModel:FirstViewModelbyviewModels()privatevalsecondViewModel:SecondViewModelbyviewModels{viewModelFactory{initializer{valsavedStateHandle=createSavedStateHandle()SecondViewModel(300,"Hello",savedStateHandle)}}}overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_second)}}这里使用了两个 viewModels 方法,其效果都是用于构建 ViewModel:
by viewModels()用于无参构造的 ViewModel,内部用默认 Factoryby viewModels { factory }用于带参构造的 ViewModel,传入自定义 Factory
这两者本质上和ViewModelProvider(this)[...]完全一样,只是 Kotlin 属性委托的语法糖而已。
ViewModel 的注意事项
一个 ViewModel 的生命周期会从其创建时持续到 Activity 真正退出,中间无论 Activity 重建多少次,其重建后拿到的 ViewModel 是同一个。而在 Activity 真正退出时,ViewModel 的onCleared()方法将会被调用。
一个 Activity 可以有多个 ViewModel,但是相同类型的 ViewModel 默认只有一个。
多个 Fragment 可以通过requireActivity()来获取同一个 ViewModel 实例,并以此来实现数据共享:
// Fragment AvalsharedViewModel:SharedViewModelbyactivityViewModels()// Fragment BvalsharedViewModel:SharedViewModelbyactivityViewModels()// 拿到的是同一个实例activityViewModels()和viewModels()类似,但它的 ViewModelStoreOwner 是requireActivity()而不是 Fragment 自己。
最后,还需要注意一点:ViewModel 不能持有 View,Activity,Fragment 引用。