ViewModel 概述 属于 Android Jetpack 的一部分。
该 ViewModel
类是一个 业务逻辑或屏幕级别状态持有者。它将状态公开给 UI 并封装相关的业务逻辑。它的主要优势在于它缓存状态并将其持久化到配置更改。这意味着您的 UI 无需在活动之间导航或在配置更改(例如旋转屏幕)后再次获取数据。
有关状态持有者的更多信息,请参阅 状态持有者 指南。同样,有关 UI 层的更多信息,请参阅 UI 层 指南。
ViewModel 的优势
ViewModel 的替代方案是持有您在 UI 中显示的数据的普通类。这在活动或导航目的地之间导航时会导致问题。如果您不使用 保存实例状态机制 存储它,这样做会销毁该数据。ViewModel 为数据持久性提供了一个方便的 API,它可以解决此问题。
ViewModel 类的关键优势本质上是两个
- 它允许您持久化 UI 状态。
- 它提供对业务逻辑的访问。
持久性
ViewModel 允许持久化 ViewModel 持有的状态以及 ViewModel 触发的操作。这种缓存意味着您无需在常见的配置更改(例如屏幕旋转)中再次获取数据。
范围
实例化 ViewModel 时,您会将一个实现 ViewModelStoreOwner
接口的对象传递给它。这可能是导航目的地、导航图、活动、片段或任何其他实现该接口的类型。然后,您的 ViewModel 的范围限定为 Lifecycle 的 ViewModelStoreOwner
。它将一直保留在内存中,直到其 ViewModelStoreOwner
永久消失。
一系列类是 ViewModelStoreOwner
接口的直接或间接子类。直接子类是 ComponentActivity
、Fragment
和 NavBackStackEntry
。有关间接子类的完整列表,请参阅 ViewModelStoreOwner
参考。
当 ViewModel 所关联的片段或活动被销毁时,在 ViewModel 中进行的异步操作将继续进行,因为它们与活动或片段关联。这就是持久化的关键。
有关更多信息,请参阅下面的 ViewModel 生命周期的部分。
SavedStateHandle
SavedStateHandle 允许您不仅在配置更改时持久化数据,而且还可以跨进程重新创建持久化数据。也就是说,即使用户关闭应用程序并在稍后重新打开,它也能让 UI 状态保持完整。
访问业务逻辑
尽管大多数 业务逻辑 存在于数据层,但 UI 层也可以包含业务逻辑。当从多个存储库组合数据以创建屏幕 UI 状态时,或者当特定类型的数据不需要数据层时,这种情况就会发生。
ViewModel 是处理 UI 层业务逻辑的正确位置。ViewModel 还负责处理事件,并在需要应用业务逻辑来修改应用程序数据时将事件委托给层次结构中的其他层。
Jetpack Compose
当使用 Jetpack Compose 时,ViewModel 是将屏幕 UI 状态公开给您的可组合函数的主要方式。在混合应用程序中,活动和片段只是托管您的可组合函数。这与过去的方法有所不同,过去的方法中,使用活动和片段创建可重用的 UI 部分并不简单直观,这导致它们更像是 UI 控制器。
在将 ViewModel 与 Compose 一起使用时,最重要的是要记住,您不能将 ViewModel 关联到可组合函数。这是因为可组合函数不是 ViewModelStoreOwner
。同一个可组合函数在 Composition 中的两个实例,或者两个不同的可组合函数在同一个 ViewModelStoreOwner
下访问同一个 ViewModel 类型,将接收 **相同** 的 ViewModel 实例,这通常不是预期行为。
要获得 Compose 中 ViewModel 的 好处,请在 Fragment 或 Activity 中托管每个屏幕,或使用 Compose Navigation,并在尽可能靠近 Navigation 目标的可组合函数中使用 ViewModel。这是因为您可以将 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
)
);
}
}
然后,您可以从活动中访问 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 状态相同的方式持久化异步操作。
有关更多信息,请参阅 在 Android 架构组件中使用 Kotlin 协程。
ViewModel 的生命周期
ViewModel
的生命周期直接与其关联的范围绑定。直到与其关联的 ViewModelStoreOwner
消失,ViewModel
才会保留在内存中。这可能会在以下情况下发生
- 在活动的情况下,当它结束时。
- 在片段的情况下,当它分离时。
- 在 Navigation 条目情况下,当它从后退栈中移除时。
这使得 ViewModel 成为存储可在配置更改后保留的数据的绝佳解决方案。
图 1 说明了活动在经过旋转并最终结束时所经历的不同生命周期状态。该图还显示了与关联的活动生命周期并列的 ViewModel
的生命周期。此特定图表说明了活动的状态。相同的基本状态适用于片段的生命周期。
通常在系统第一次调用活动对象的 onCreate()
方法时,您会请求一个 ViewModel
。系统可能会在活动的整个生命周期中多次调用 onCreate()
,例如当设备屏幕旋转时。从您第一次请求 ViewModel
开始,直到活动结束并被销毁,ViewModel
将一直存在。
清除 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 作为屏幕级状态持有者的实现细节。不要将它们用作可重用 UI 组件(例如芯片组或表单)的状态持有者。否则,除非您对每个芯片使用显式的视图模型键,否则您将在同一个 ViewModelStoreOwner 下使用同一个 UI 组件的不同情况下获得同一个 ViewModel 实例。
- ViewModel 不应该了解 UI 实现细节。请尽可能保持 ViewModel API 公开的函数名称和 UI 状态字段名称的通用性。这样,您的 ViewModel 就可以适应任何类型的 UI:手机、可折叠设备、平板电脑,甚至 Chromebook!
- 由于 ViewModel 可能比
ViewModelStoreOwner
的生命周期更长,因此 ViewModel 不应持有任何生命周期相关 API(如Context
或Resources
)的引用,以防止内存泄漏。 - 不要将 ViewModel 传递给其他类、函数或其他 UI 组件。由于平台会管理它们,因此您应该将它们尽可能地靠近平台。靠近您的 Activity、Fragment 或屏幕级可组合函数。这可以防止较低级别的组件访问超过其所需的数据和逻辑。
进一步的信息
随着数据的变得越来越复杂,您可能选择创建一个单独的类来加载数据。 ViewModel
的目的是封装 UI 控制器的关联数据,使数据在配置更改后能够保留。有关如何在配置更改后加载、持久化和管理数据的更多信息,请参阅 已保存的 UI 状态。
Android 应用程序架构指南 建议构建一个存储库类来处理这些功能。
其他资源
有关 ViewModel
类的更多信息,请查阅以下资源。
文档
示例
推荐给您
- 注意:当 JavaScript 关闭时,链接文本将显示
- 将 Kotlin 协程与生命周期感知组件一起使用
- 保存 UI 状态
- 加载和显示分页数据