ViewModel 概览   Android Jetpack 的一部分。

试用 Kotlin Multiplatform
Kotlin Multiplatform 允许与其他平台共享业务逻辑。了解如何在 KMP 中设置和使用 ViewModel

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

如需详细了解状态容器,请参阅状态容器指南。 同样,如需详细了解有关界面层的一般信息,请参阅界面层指南。

ViewModel 的优势

ViewModel 的替代方案是保存要在界面中显示的数据的普通类。在 activity 或 Navigation 目的地之间导航时,这可能会造成问题。此时,如果您不利用保存实例状态机制存储相应数据,系统便会销毁相应数据。ViewModel 提供了一个便捷的数据持久性 API,可以解决此问题。

或者,对于纯状态容器,Compose 提供了 retain 功能,允许普通类在没有 ViewModel 的完整基础架构的情况下在配置更改后继续存在。虽然这两种机制都有助于保留状态,但通常更安全的做法是为保留的实例提供 ViewModel,而不是反过来,因为它们的生命周期和清理行为不同。

ViewModel 类的主要优势实际上有两个方面:

  • 它允许您持久保留界面状态。
  • 它可以提供对业务逻辑的访问权限。

持久性

ViewModel 允许数据在 ViewModel 持有的状态和 ViewModel 触发的操作结束后继续存在。这种缓存意味着在常见的配置更改(例如屏幕旋转)完成后,您无需重新提取数据。

范围

实例化 ViewModel 时,您会向其传递实现 ViewModelStoreOwner 接口的对象。它可能是 Navigation 目的地、Navigation 图表、activity 或实现接口的任何其他类型。您还可以使用 rememberViewModelStoreOwner API 将 ViewModel 直接限定到可组合项。 然后,ViewModel 的作用域将限定为 LifecycleViewModelStoreOwner。它会一直保留在内存中,直到其 ViewModelStoreOwner 永久消失(例如,当可组合项所有者退出组合时)。

有一系列类是 ViewModelStoreOwner 接口的直接或间接子类。直接子类是 ComponentActivityNavBackStackEntry。 如需查看间接子类的完整列表,请参阅 ViewModelStoreOwner参考文档。如需将 ViewModel 限定到 LazyListPager 中的各个项,请使用 rememberViewModelStoreProvider() 将所有者管理提升到父级。

当宿主 activity 发生配置更改时,无论 ViewModel 的作用域是 activity 还是特定可组合项,异步工作都会在 ViewModel 中继续进行。这是持久性的关键。

如需了解详情,请参阅以下 ViewModel 生命周期 部分、 ViewModel 作用域 API,以及有关 Jetpack Compose 的 状态提升 的指南。

SavedStateHandle

借助 SavedStateHandle,您不仅可以在更改配置 后持久保留数据,还可以在进程终止后持久保留数据。也就是说,即使用户关闭应用,稍后又将其打开,您的界面状态也可以保持不变。

如需详细了解如何保存界面状态, 请参阅在 Compose 中保存界面状态

对业务逻辑的访问权限

尽管绝大多数业务逻辑都存在于数据层中,但界面层也可以包含业务逻辑。当您合并多个代码库中的数据以创建屏幕界面状态时,或特定类型的数据不需要数据层时,情况就是如此。

ViewModel 是在界面层处理业务逻辑的正确位置。当需要应用业务逻辑来修改应用数据时,ViewModel 还负责处理事件并将其委托给层次结构中的其他层。

实现 ViewModel

以下是用户掷骰子屏幕的 ViewModel 实现示例。

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

然后,您可以从屏幕级可组合项访问 ViewModel,如下所示:

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

将协程与 ViewModel 一起使用

ViewModel 支持 Kotlin 协程。它能够像持久保留界面状态一样持久保留异步工作。

如需了解详情,请参阅将 Kotlin 协程与 Android 架构组件一起使用

ViewModel 的生命周期

ViewModel 的生命周期与其作用域直接关联。ViewModel 会一直保留在内存中,直到其作用域 ViewModelStoreOwner 消失。以下上下文中可能会发生这种情况:

  • 对于 activity,是在 activity 完成时。
  • 对于 Navigation 条目,是在 Navigation 条目从返回堆栈中移除时。
  • 对于可组合项,是在可组合项退出组合时。 您可以使用 rememberViewModelStoreOwner 将 ViewModel 直接限定到界面的任意部分(例如 PagerLazyList)。

这使得 ViewModels 成为了存储在配置更改后仍然存在的数据的绝佳解决方案。

图 1 说明了 activity 经历屏幕旋转而后结束时所处的各种生命周期状态。该图还在关联的 activity 生命周期旁边显示了 ViewModel的生命周期。此图表说明了 activity 的各种状态。

说明 ViewModel 随着 Activity 状态的改变而经历的生命周期。
图 1.activity 和 ViewModel 的生命周期状态。

您通常在系统首次调用 activity 对象的 onCreate() 方法时请求 ViewModel。系统可能会在 activity 的整个生命周期内多次调用 onCreate(),如在旋转设备屏幕时。ViewModel 存在的时间范围是从您首次请求 ViewModel 直到 activity 完成并销毁。

清除 ViewModel 依赖项

ViewModelStoreOwner 在 ViewModel 的生命周期内销毁 ViewModel 时,ViewModel 会调用 onCleared 方法。这样,您就可以清理遵循 ViewModel 生命周期的任何工作或依赖项。

以下示例展示了 viewModelScope 的替代方法。 viewModelScope 是一个内置 CoroutineScope,会自动遵循 ViewModel 的生命周期。ViewModel 使用 viewModelScope 触发与业务相关的操作。如果您想使用自定义作用域(而不是 viewModelScope)使测试更简单,ViewModel 可以在其构造函数中接收 CoroutineScope 作为依赖项。如果 ViewModelStoreOwner 在 ViewModel 的生命周期结束时清除 ViewModel,ViewModel 也会取消 CoroutineScope

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

从 Lifecycle 版本 2.5 及更高版本开始, 您可以将一个或多个 Closeable 对象传递给在清除 ViewModel 实例时自动关闭的 ViewModel 构造函数。

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

最佳实践

以下是实现 ViewModel 时应遵循的一些重要的最佳实践:

  • 由于 ViewModel 的作用域,请使用 ViewModel 作为屏幕级状态容器的实现细节。请勿将它们用条状标签组或表单等可重复使用的界面组件的状态容器。否则,当您在同一 ViewModelStoreOwner 下将同一界面组件用于不同用途时,您会获取相同的 ViewModel 实例,除非您为每个条状标签使用显式视图模型键。
  • ViewModel 不应该知道界面实现细节。请尽可能对 ViewModel API 公开的方法和界面状态字段使用通用名称。这样一来,ViewModel 便可以适应任何类型的界面:手机、可折叠设备、平板电脑甚至 Chromebook!
  • 由于 ViewModel 的生命周期可能比 ViewModelStoreOwner 更长,因此 ViewModel 不应保留任何对与生命周期相关的 API(例如 ContextResources)的引用,以免发生内存泄漏。
  • 请勿将 ViewModel 传递给其他类、函数或其他界面组件。 由于平台会管理它们,因此您应该使其尽可能靠近平台,即靠近您的 activity、屏幕级可组合函数或 Navigation 目的地。 这样可以防止较低级别的组件访问超出其需求的数据和逻辑。

更多信息

随着数据变得越来越复杂,您可能会选择使用单独的类加载数据。ViewModel 的用途是封装界面控制器的数据,以使数据在配置更改后仍然存在。如需了解如何在配置更改后加载、保留和管理数据,请参阅保存的界面状态

Android 应用架构指南建议构建存储库类来处理这些功能。

其他资源

如需详细了解 ViewModel 类,请参阅以下资源。

文档

查看内容

示例