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 并在尽可能接近导航目标的可组合函数中使用 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
消失。这可能发生在以下情况下:
- 对于Activity,当它结束时。
- 对于Fragment,当它分离时。
- 对于导航条目,当它从返回堆栈中移除时。
这使得ViewModel成为存储可在配置更改后仍然存在的数据的绝佳解决方案。
图1说明了Activity在旋转后并最终结束时的各种生命周期状态。该图还显示了与关联的Activity生命周期并列的ViewModel
的生命周期。此特定图表说明了Activity的状态。相同的基本状态也适用于Fragment的生命周期。
您通常在系统首次调用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版及更高版本开始,您可以将一个或多个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、Fragment或屏幕级可组合函数。这可以防止较低级别的组件访问超出其所需的数据和逻辑。
更多信息
随着数据变得越来越复杂,您可能选择使用单独的类来加载数据。ViewModel
的目的是封装UI控制器的数 据,以使数据在配置更改后仍然存在。有关如何在配置更改中加载、持久化和管理数据的更多信息,请参阅保存的UI状态。
Android应用架构指南建议构建一个存储库类来处理这些函数。
其他资源
有关ViewModel
类的更多信息,请参阅以下资源。
文档
示例
为您推荐
- 注意:当JavaScript关闭时显示链接文本
- 将 Kotlin 协程与生命周期感知组件一起使用
- 保存 UI 状态
- 加载和显示分页数据