ViewModel 概览   Android Jetpack 的一部分。

ViewModel 类是一种业务逻辑或屏幕级状态容器。它向 UI 暴露状态,并封装相关的业务逻辑。其主要优势在于它可以缓存状态,并在配置更改(例如旋转屏幕)时保留状态。这意味着在 activity 之间导航或发生配置更改后,您的 UI 不必重新获取数据。

如需详细了解状态容器,请参阅状态容器指南。同样,如需详细了解 UI 层,请参阅UI 层指南。

ViewModel 的优势

ViewModel 的替代方案是一个普通类,用于存放您在 UI 中显示的数据。这在 activity 或 Navigation 目的地之间导航时可能会成为问题。如果您不使用保存实例状态机制存储数据,这样做会销毁数据。ViewModel 提供了一个便捷的数据持久化 API,可以解决这个问题。

ViewModel 类的主要优势基本上有两点

  • 它允许您保留 UI 状态。
  • 它提供了对业务逻辑的访问。

持久性

ViewModel 既可以通过 ViewModel 持有的状态,也可以通过 ViewModel 触发的操作来实现持久性。这种缓存机制意味着您无需在屏幕旋转等常见的配置更改时再次获取数据。

作用域

实例化 ViewModel 时,您会向其传递一个实现 ViewModelStoreOwner 接口的对象。这可以是 Navigation 目的地、Navigation 图、activity、fragment 或任何其他实现该接口的类型。然后,您的 ViewModel 的作用域限定在其 ViewModelStoreOwnerLifecycle 内。它将一直保留在内存中,直到其 ViewModelStoreOwner 永久消失为止。

许多类是 ViewModelStoreOwner 接口的直接或间接子类。直接子类包括 ComponentActivityFragmentNavBackStackEntry。如需查看间接子类的完整列表,请参阅 ViewModelStoreOwner 参考文档

当 ViewModel 的作用域所在的 fragment 或 activity 被销毁时,作用域限定在其内的 ViewModel 中的异步工作会继续。这是实现持久性的关键。

如需了解详情,请参阅下方关于ViewModel 生命周期的部分。

SavedStateHandle

SavedStateHandle 不仅可以在配置更改时保留数据,还可以在进程重建时保留数据。也就是说,即使在用户关闭应用并在稍后重新打开时,它也能保持 UI 状态完好无损。

访问业务逻辑

尽管绝大多数业务逻辑位于数据层,但 UI 层也可以包含业务逻辑。这可能发生在组合多个仓库的数据以创建屏幕 UI 状态时,或者当特定类型的数据不需要数据层时。

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

Jetpack Compose

使用 Jetpack Compose 时,ViewModel 是将屏幕 UI 状态暴露给您的可组合项的主要方式。在混合应用中,activity 和 fragment 只需托管您的可组合函数。这与过去的方法不同,过去使用 activity 和 fragment 创建可重用 UI 组件并不简单直观,这导致它们作为 UI 控制器的活跃度更高。

在使用 ViewModel 与 Compose 时,最重要的一点是不能将 ViewModel 的作用域限定在可组合项中。这是因为可组合项不是 ViewModelStoreOwner。Composition 中同一可组合项的两个实例,或在同一 ViewModelStoreOwner 下访问同一 ViewModel 类型的两个不同可组合项,都将接收到 ViewModel 的同一个实例,这通常不是预期的行为。

要在 Compose 中获得 ViewModel 的优势,请在 Fragment 或 Activity 中托管每个屏幕,或使用 Compose Navigation 并在可组合函数中使用 ViewModel,使其尽可能靠近 Navigation 目的地。这是因为您可以将 ViewModel 的作用域限定在 Navigation 目的地、Navigation 图、Activity 和 Fragment 中。

如需详细了解 Jetpack Compose 中的状态提升,请参阅相关指南。

实现 ViewModel

以下是一个 ViewModel 的实现示例,用于允许用户掷骰子的屏幕。

Kotlin

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,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

然后,您可以按如下方式从 activity 访问 ViewModel

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

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 协程。它能够以与保留 UI 状态相同的方式保留异步工作。

如需了解详情,请参阅将 Kotlin 协程与 Android Architecture Components 结合使用

ViewModel 的生命周期

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

  • 对于 activity,当其结束时。
  • 对于 fragment,当其分离时。
  • 对于 Navigation 条目,当其从返回栈中移除时。

这使得 ViewModel 成为存储可在配置更改中保留的数据的绝佳解决方案。

图 1 说明了 activity 在旋转然后结束时经历的各种生命周期状态。该图还显示了 ViewModel 的生命周期以及关联的 activity 生命周期。这个特定图表说明了 activity 的状态。相同的基本状态也适用于 fragment 的生命周期。

Illustrates the lifecycle of a ViewModel as an activity changes state.

您通常在系统首次调用 activity 对象的 onCreate() 方法时请求 ViewModel。在 activity 的整个生命周期中,系统可能会多次调用 onCreate(),例如当设备屏幕旋转时。ViewModel 的生命周期从您首次请求 ViewModel 时开始,直到 activity 结束并销毁为止。

清除 ViewModel 依赖项

ViewModelStoreOwner 在其生命周期中销毁 ViewModel 时,ViewModel 会调用 onCleared 方法。这允许您清理任何遵循 ViewModel 生命周期的工作或依赖项。

以下示例展示了 viewModelScope 的替代方案。viewModelScope 是一个内置的 CoroutineScope,它会自动遵循 ViewModel 的生命周期。ViewModel 使用它来触发与业务相关的操作。如果您想使用自定义作用域而不是 viewModelScope 以便更轻松地进行测试,则 ViewModel 可以在其构造函数中接收 CoroutineScope 作为依赖项。当 ViewModelStoreOwner 在其生命周期结束时清除 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 及更高版本开始,您可以向 ViewModel 的构造函数传递一个或多个 Closeable 对象,这些对象在 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 用作屏幕级状态容器的实现细节。不要将它们用作可重用 UI 组件(例如芯片组或表单)的状态容器。否则,您将在同一 ViewModelStoreOwner 下同一 UI 组件的不同用法中获得同一个 ViewModel 实例,除非您为每个芯片使用显式的 view model key。
  • ViewModel 不应该知道 UI 实现的细节。ViewModel API 暴露的方法名称和 UI 状态字段的名称应尽可能通用。这样,您的 ViewModel 就可以适应任何类型的 UI:手机、可折叠设备、平板电脑,甚至是 Chromebook!
  • 由于 ViewModel 的生命周期可能比 ViewModelStoreOwner 长,因此 ViewModel 不应持有任何与生命周期相关的 API(例如 ContextResources)的引用,以防止内存泄漏。
  • 不要将 ViewModel 传递给其他类、函数或其他 UI 组件。由于平台管理它们,您应该让它们尽可能靠近平台。靠近您的 Activity、fragment 或屏幕级可组合函数。这可以防止较低级别的组件访问超出其需求的数据和逻辑。

更多信息

随着数据变得越来越复杂,您可能选择使用一个单独的类来加载数据。ViewModel 的目的是封装 UI 控制器的数据,让数据在配置更改中得以保留。有关如何在配置更改时加载、保留和管理数据的信息,请参阅保存的 UI 状态

Android 应用架构指南》建议构建一个仓库类来处理这些函数。

其他资源

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

文档

示例