ViewModel 概览 属于 Android Jetpack。
The ViewModel
类是一种业务逻辑或屏幕级状态容器。它向界面公开状态并封装相关业务逻辑。它的主要优点是它可以缓存状态并在配置更改时持久保存该状态。这意味着您的界面在 activity 之间导航或遵循配置更改(例如旋转屏幕)时,无需再次获取数据。
如需详细了解状态容器,请参阅状态容器指南。同样,如需详细了解界面层,请参阅界面层指南。
ViewModel 的优势
ViewModel 的替代方案是一个普通类,用于保存您在界面中显示的数据。在 activity 或导航目标之间导航时,这可能会成为一个问题。如果您不使用保存实例状态机制存储数据,这样做会销毁数据。ViewModel 提供了一个方便的数据持久化 API,可解决此问题。
ViewModel 类的主要优势基本有两点
- 它允许您持久化界面状态。
- 它提供了对业务逻辑的访问。
持久性
ViewModel 允许通过 ViewModel 持有的状态和 ViewModel 触发的操作来实现持久性。这种缓存意味着您无需在常见的配置更改(例如屏幕旋转)时再次获取数据。
作用域
实例化 ViewModel 时,您会向其传递一个实现 ViewModelStoreOwner
接口的对象。这可能是导航目标、导航图、activity、fragment 或任何其他实现该接口的类型。您的 ViewModel 的作用域随后会限定到 ViewModelStoreOwner
的 生命周期。它会一直保留在内存中,直到其 ViewModelStoreOwner
永久消失。
一系列类是 ViewModelStoreOwner
接口的直接或间接子类。直接子类包括 ComponentActivity
、Fragment
和 NavBackStackEntry
。如需间接子类的完整列表,请参阅 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 的生命周期。
通常,当系统首次调用 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 引用(例如Context
或Resources
),以防止内存泄漏。 - 不要将 ViewModel 传递给其他类、函数或其他界面组件。由于平台管理它们,您应该将它们尽可能地靠近平台。靠近您的 Activity、fragment 或屏幕级可组合函数。这可以防止较低级别的组件访问超出其所需的数据和逻辑。
更多信息
随着您的数据变得更加复杂,您可能选择只使用一个单独的类来加载数据。ViewModel
的目的是封装界面控制器的数据,让数据在配置更改后仍能保留。有关如何在配置更改后加载、持久化和管理数据的信息,请参阅已保存的界面状态。
Android 应用架构指南建议构建一个代码库类来处理这些函数。
其他资源
如需详细了解 ViewModel
类,请查阅以下资源。
文档
示例
为您推荐
- 注意:当 JavaScript 关闭时会显示链接文本
- 将 Kotlin 协程与感知生命周期的组件结合使用
- 保存界面状态
- 加载和显示分页数据