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
。组合中的同一可组合项的两个实例,或在同一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
的生命周期与其作用域直接相关。ViewModel
会一直保留在内存中,直到其作用域的ViewModelStoreOwner
消失。这可能在以下情况下发生
- 对于活动,在其完成时。
- 对于片段,在其分离时。
- 对于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!
- 由于它们的生命周期可能长于
ViewModelStoreOwner
,因此ViewModel不应持有任何与生命周期相关的API的引用,例如Context
或Resources
,以防止内存泄漏。 - 不要将ViewModel传递给其他类、函数或其他UI组件。由于平台会管理它们,因此您应尽可能使它们靠近平台。靠近您的Activity、片段或屏幕级可组合函数。这可以防止较低级别的组件访问超出其所需的数据和逻辑。
更多信息
随着数据变得越来越复杂,您可能可以选择使用单独的类来加载数据。ViewModel
的目的是封装UI控制器的數據,以便数据在配置更改后继续存在。有关如何在配置更改之间加载、持久化和管理数据的更多信息,请参阅保存的UI状态。
Android应用程序架构指南建议构建一个存储库类来处理这些函数。
其他资源
有关ViewModel
类的更多信息,请查阅以下资源。
文档
示例
为您推荐
- 注意:关闭JavaScript时会显示链接文本
- 将 Kotlin 协程与生命周期感知组件一起使用
- 保存 UI 状态
- 加载和显示分页数据