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

ViewModel 是 Jetpack 中非常重要的一个库,它与 Lifecycle、LiveData 构建了 MAD 的三大基础组件。在 ViewModel 之前,Activity 负责处理所有内容,这使得其变得极为庞大,并且需要负责由于其混乱的生命周期带来的一系列问题。因此,ViewModel 在推出之后就以摧枯拉朽之势席卷了整个 Android 开发界。

本文将带你走进 ViewModel,看看它的前世今生,以及使用方法。阅读此文,你必将对 ViewModel 有一个新的认识。不过为了说明 ViewModel 的作用,我们需要回到 Jetpack 之前那些没有 ViewModel 的日子里。

在 ViewModel 之前

在 ViewModel 出现之前,Android 开发中 Activity 既要管 UI 显示,又要管数据获取和处理。在这时,一个典型的登录页 Activity 大概率会是这个样子:

kotlin 复制代码
class LoginActivity : Activity() {
    private lateinit var etUsername: EditText
    private lateinit var etPassword: EditText
    private lateinit var progressBar: ProgressBar

    override fun onCreate(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)
    }

    fun onLoginClick(view: View) {
        val username = etUsername.text.toString()
        val password = etPassword.text.toString()

        progressBar.visibility = View.VISIBLE

        // 直接在 Activity 里发起网络请求
        Thread {
            val success = loginApi.login(username, password)
            runOnUiThread {
                progressBar.visibility = View.GONE
                if (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 发生重建,这种代码就是非常有问题的。

kotlin 复制代码
// Activity 已经被销毁,但回调还在持有 this
Thread {
    val result = 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 中声明依赖:

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 引入依赖:

kotlin 复制代码
dependencies {
    implementation(libs.androidx.activity.ktx)
    implementation(libs.androidx.lifecycle.viewmodel.ktx)
    implementation(libs.androidx.lifecycle.viewmodel.savedstate)
}

基本用法

创建 ViewModel

kotlin 复制代码
class FirstViewModel : ViewModel() {
    
    val textLiveData = MutableLiveData<String>()
    
    override fun onCleared() {
        super.onCleared()
        // Activity 真正退出时(非配置变更)自动调用
    }
}

在 Activity 中获取

kotlin 复制代码
class MainActivity : ComponentActivity() {

    private lateinit var viewModel: FirstViewModel

    override fun onCreate(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:

kotlin 复制代码
class SecondViewModel(
    private val initC: Int,
    val initS: String,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    var counter: Int
        get() = savedStateHandle["counter"] ?: initC
        set(value) { savedStateHandle["counter"] = value }
}

为了创建这样的 ViewModel,我们必须提供 Factory 以供系统调用,毕竟这种带参数的 ViewModel,只有我们知道如何创建。

创建这个 ViewModel 的代码如下:

kotlin 复制代码
viewModel = ViewModelProvider(this, viewModelFactory {
    initializer {
        val savedStateHandle = createSavedStateHandle()
        SecondViewModel(300, "Hello", savedStateHandle)
    }
})[SecondViewModel::class.java]

带 Application 的 ViewModel

前面创建的都是没有带 Context 的 ViewModel,但是如果 ViewModel 需要 Context 怎么办?面对这个问题,Google 提供了 AndroidViewModel------就是 ViewModel 的一个子类,多持有一个 Application 引用:

kotlin 复制代码
class MyAndroidViewModel(application: Application) : AndroidViewModel(application) {
    fun doSomething() {
        val context = getApplication<Application>()
        // ...
    }
}

注意这里只能用 Application 的 Context,不能用 Activity 的 Context。原因就是 ViewModel 不能持有 Activity 引用,否则配置变更时旧 Activity 被销毁,ViewModel 还持有它的引用,导致内存泄漏。

使用扩展方法 viewModels() 进行创建

创建 ViewModel 不只是可以像上面那样使用 ViewModelProvider 来创建,还可以通过 ComponentActivity 的扩展方法 viewModels 来创建:

kotlin 复制代码
class MainActivity : ComponentActivity() {

    private val firstViewModel: FirstViewModel by viewModels()
    private val secondViewModel: SecondViewModel by viewModels {
        viewModelFactory {
            initializer {
                val savedStateHandle = createSavedStateHandle()
                SecondViewModel(300, "Hello", savedStateHandle)
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
    }
}        

这里使用了两个 viewModels 方法,其效果都是用于构建 ViewModel:

  • by viewModels() 用于无参构造的 ViewModel,内部用默认 Factory
  • by viewModels { factory } 用于带参构造的 ViewModel,传入自定义 Factory

这两者本质上和 ViewModelProvider(this)[...] 完全一样,只是 Kotlin 属性委托的语法糖而已。

ViewModel 的注意事项

一个 ViewModel 的生命周期会从其创建时持续到 Activity 真正退出,中间无论 Activity 重建多少次,其重建后拿到的 ViewModel 是同一个。而在 Activity 真正退出时,ViewModel 的 onCleared() 方法将会被调用。

一个 Activity 可以有多个 ViewModel,但是相同类型的 ViewModel 默认只有一个。

多个 Fragment 可以通过 requireActivity() 来获取同一个 ViewModel 实例,并以此来实现数据共享:

kotlin 复制代码
// Fragment A
val sharedViewModel: SharedViewModel by activityViewModels()
// Fragment B
val sharedViewModel: SharedViewModel by activityViewModels()
// 拿到的是同一个实例

activityViewModels()viewModels() 类似,但它的 ViewModelStoreOwner 是 requireActivity() 而不是 Fragment 自己。

最后,还需要注意一点:ViewModel 不能持有 View,Activity,Fragment 引用。

相关推荐
2501_915909061 小时前
iOS应用性能监控 Instruments工具与崩溃日志分析完整指南
android·ios·小程序·https·uni-app·iphone·webview
杉氧2 小时前
弹性滚动的奥秘:在 Flutter 中使用 CustomScrollView 与 Sliver 打造极致流畅列表
android·前端·flutter
黄林晴2 小时前
你敢信吗?R8让 Compose 协程直接快 2 倍!你敢相信吗?R8 让合成程度直接协调快速 2 倍!
android
张风捷特烈3 小时前
Flutter UI 解耦 - 天下大势,合久必分, 分久必合
android·前端·flutter
蓝速科技3 小时前
蓝速科技丨甲级写字楼会议预约屏尺寸选型与空间适配指南
android·科技
zhangjin11206 小时前
解决Android Studio gradle下载超时和缓慢问题(二)
android·ide·android studio
xingxiliang14 小时前
深入浅出 Android CTS 视频测试:从环境搭建、用例设计到底层通信原理
android·音视频
我命由我1234514 小时前
Android 开发问题:为 PDFView 设置一个带有黑色边框的背景 drawable,但边框没有生效
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
雨白15 小时前
深入理解 Kotlin 协程 (八):拾遗补阙,探秘官方框架的调度细节与取消闭环
android·kotlin