ViewModel 概述 是 Android Jetpack 的一部分。
The ViewModel
class is a business logic or screen level state holder. It exposes state to the UI and encapsulates related business logic. Its principal advantage is that it caches state and persists it through configuration changes. This means that your UI doesn’t have to fetch data again when navigating between activities, or following configuration changes, such as when rotating the screen.
有关状态持有者的更多信息,请参阅 状态持有者 指南。类似地,有关 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 的相同实例,这通常不是预期行为。
要获得 ViewModel 在 Compose 中的 优势,请在片段或活动中托管每个屏幕,或使用 Compose Navigation,并在可组合函数中尽可能靠近导航目标使用 ViewModel。这是因为您可以将 ViewModel 限定到导航目标、导航图、活动和片段。
有关更多信息,请参阅 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
的生命周期直接与其范围绑定。一个 ViewModel
将一直保存在内存中,直到其限定的 ViewModelStoreOwner
消失。这可能在以下情况下发生
- 在活动的情况下,当活动完成时。
- 在片段的情况下,当片段分离时。
- 在导航条目情况下,当它从后退栈中移除时。
这使得 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!
- 由于它们可能比
ViewModelStoreOwner
存活时间更长,因此 ViewModel 不应该持有任何与生命周期相关的 API 的引用,例如Context
或Resources
,以防止内存泄漏。 - 不要将 ViewModel 传递给其他类、函数或其他 UI 组件。由于平台对其进行管理,因此您应该尽可能地将它们靠近平台。靠近您的活动、片段或屏幕级可组合函数。这可以防止较低级别的组件访问超出其需要的更多数据和逻辑。
更多信息
随着数据的增长变得更加复杂,您可能选择拥有一个单独的类来专门加载数据。 ViewModel
的目的是封装 UI 控制器的數據,以使数据能够在配置更改中生存。有关如何在配置更改中加载、持久化和管理数据的更多信息,请参阅 保存的 UI 状态。
Android 应用架构指南 建议构建一个存储库类来处理这些功能。
其他资源
有关 ViewModel
类的更多信息,请参阅以下资源。
文档
示例
为您推荐
- 注意:当 JavaScript 关闭时,链接文本会显示
- 使用 Kotlin 协程与生命周期感知组件
- 保存 UI 状态
- 加载和显示分页数据