Jetpack UI状态模型 ViewModel 的入门和使用

Jetpack UI状态模型 ViewModel 的入门和使用 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)etUsernamefindViewById(R.id.et_username)etPasswordfindViewById(R.id.et_password)progressBarfindViewById(R.id.progress)}funonLoginClick(view:View){valusernameetUsername.text.toString()valpasswordetPassword.text.toString()progressBar.visibilityView.VISIBLE// 直接在 Activity 里发起网络请求Thread{valsuccessloginApi.login(username,password)runOnUiThread{progressBar.visibilityView.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{valresultapi.fetchData()runOnUiThread{// Activity 可能已经 finish 了textView.textresult.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 长配置变更时不会被销毁数据自动保留异步回调引用已死 ActivityViewModel 持有数据Activity 重建后重新观察 ViewModel不存在回调引用旧 Activity的问题Activity 巨大不可测试业务逻辑搬到 ViewModelViewModel 不持有 Android 框架类View、Activity可以直接写单元测试这里需要注意的一点是Activity 通过观察 ViewModel 中的数据来展示界面但 ViewModel 仅持有数据因此为了观察数据使用 ViewModel 往往需要搭配 LiveData 或是 Flow 来使用而为了保证对 Activity 生命周期的正确处理还需要依赖 Lifecycle 库。对这两个库不了解的可以看一下以下两篇文章Jetpack 生命周期组件 Lifecycle 的设计思想和使用Jetpack 可观察数据容器 LiveData 的入门与基础使用为什么叫做 ViewModelViewModel 这个名字不是 Google 发明的它来自微软的 MVVM 模式Model-View-ViewModel2005 年由微软的 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)}基本用法创建 ViewModelclassFirstViewModel:ViewModel(){valtextLiveDataMutableLiveDataString()overridefunonCleared(){super.onCleared()// Activity 真正退出时非配置变更自动调用}}在 Activity 中获取classMainActivity:ComponentActivity(){privatelateinitvarviewModel:FirstViewModeloverridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_second)textViewfindViewById(R.id.text_view)viewModelViewModelProvider(this)[FirstViewModel::class.java]viewModel.textLiveData.observe(this){textView.textit}}}这里需要注意的是我们创建 viewModel 使用的是ViewModelProvider它只是一个工具类我们使用它创建 ViewModel不需要持有其引用。而其会调用this这里是 ComponentActivity的默认 Factory用于创建 ViewModel。所以这里创建 ViewModel 的代码很简单。一旦持有了一个 ViewModel 后就可以观察 ViewModel 中的 LiveData 来同步到界面上了。带构造参数的 ViewModel默认 Factory 只能调用无参构造函数。如果 ViewModel 有构造参数需要自定义 Factory。例如下面的 ViewModelclassSecondViewModel(privatevalinitC:Int,valinitS:String,privatevalsavedStateHandle:SavedStateHandle):ViewModel(){varcounter:Intget()savedStateHandle[counter]?:initCset(value){savedStateHandle[counter]value}}为了创建这样的 ViewModel我们必须提供 Factory 以供系统调用毕竟这种带参数的 ViewModel只有我们知道如何创建。创建这个 ViewModel 的代码如下viewModelViewModelProvider(this,viewModelFactory{initializer{valsavedStateHandlecreateSavedStateHandle()SecondViewModel(300,Hello,savedStateHandle)}})[SecondViewModel::class.java]带 Application 的 ViewModel前面创建的都是没有带 Context 的 ViewModel但是如果 ViewModel 需要 Context 怎么办面对这个问题Google 提供了 AndroidViewModel——就是 ViewModel 的一个子类多持有一个 Application 引用classMyAndroidViewModel(application:Application):AndroidViewModel(application){fundoSomething(){valcontextgetApplicationApplication()// ...}}注意这里只能用 Application 的 Context不能用 Activity 的 Context。原因就是 ViewModel 不能持有 Activity 引用否则配置变更时旧 Activity 被销毁ViewModel 还持有它的引用导致内存泄漏。使用扩展方法 viewModels() 进行创建创建 ViewModel 不只是可以像上面那样使用 ViewModelProvider 来创建还可以通过 ComponentActivity 的扩展方法viewModels来创建classMainActivity:ComponentActivity(){privatevalfirstViewModel:FirstViewModelbyviewModels()privatevalsecondViewModel:SecondViewModelbyviewModels{viewModelFactory{initializer{valsavedStateHandlecreateSavedStateHandle()SecondViewModel(300,Hello,savedStateHandle)}}}overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_second)}}这里使用了两个 viewModels 方法其效果都是用于构建 ViewModelby 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 不能持有 ViewActivityFragment 引用。