ViewModel 概览   属于 Android Jetpack

The ViewModel 类是一种业务逻辑或屏幕级状态容器。它向界面公开状态并封装相关业务逻辑。它的主要优点是它可以缓存状态并在配置更改时持久保存该状态。这意味着您的界面在 activity 之间导航或遵循配置更改(例如旋转屏幕)时,无需再次获取数据。

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

ViewModel 的优势

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

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

  • 它允许您持久化界面状态。
  • 它提供了对业务逻辑的访问。

持久性

ViewModel 允许通过 ViewModel 持有的状态和 ViewModel 触发的操作来实现持久性。这种缓存意味着您无需在常见的配置更改(例如屏幕旋转)时再次获取数据。

作用域

实例化 ViewModel 时,您会向其传递一个实现 ViewModelStoreOwner 接口的对象。这可能是导航目标、导航图、activity、fragment 或任何其他实现该接口的类型。您的 ViewModel 的作用域随后会限定到 ViewModelStoreOwner生命周期。它会一直保留在内存中,直到其 ViewModelStoreOwner 永久消失。

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

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

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

SavedStateHandle

SavedStateHandle 不仅允许您通过配置更改来持久保存数据,还允许您跨进程重新创建来持久保存数据。也就是说,即使当用户关闭应用并在稍后再次打开时,它也能让界面状态保持不变。

对业务逻辑的访问

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

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

Jetpack Compose

使用 Jetpack Compose 时,ViewModel 是将屏幕界面状态公开给可组合项的主要方式。在混合应用中,activity 和 fragment 只需托管您的可组合函数。这与过去的方法有所不同,过去使用 activity 和 fragment 创建可重用界面片段并不那么简单直观,这使得它们作为界面控制器更加活跃。

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

要在 Compose 中获得 ViewModel 的优势,请在 Fragment 或 Activity 中托管每个屏幕,或者使用 Compose Navigation 并在可组合函数中使用 ViewModel,使其尽可能靠近导航目标。这是因为您可以将 ViewModel 的作用域限定到导航目标、导航图、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 协程。它能够以持久化界面状态的相同方式持久化异步工作。

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

ViewModel 的生命周期

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

  • 对于 activity,当它完成时。
  • 对于 fragment,当它分离时。
  • 对于导航条目,当它从返回堆栈中移除时。

这使得 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()
    }
}

从生命周期版本 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 用作屏幕级状态容器的实现细节。不要将它们用作可重用界面组件(例如芯片组或表单)的状态容器。否则,在同一 ViewModelStoreOwner 下,在同一界面组件的不同用法中,您将获得相同的 ViewModel 实例,除非您为每个芯片使用显式视图模型键。
  • ViewModel 不应了解界面实现细节。尽可能保持 ViewModel API 公开的方法名称和界面状态字段的名称通用。这样,您的 ViewModel 就可以适应任何类型的界面:手机、折叠屏、平板电脑,甚至 Chromebook!
  • 由于 ViewModel 的生命周期可能比 ViewModelStoreOwner 长,因此 ViewModel 不应持有任何与生命周期相关的 API 引用(例如 ContextResources),以防止内存泄漏。
  • 不要将 ViewModel 传递给其他类、函数或其他界面组件。由于平台管理它们,您应该将它们尽可能地靠近平台。靠近您的 Activity、fragment 或屏幕级可组合函数。这可以防止较低级别的组件访问超出其所需的数据和逻辑。

更多信息

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

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

其他资源

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

文档

示例