两种状态管理方法概述
在 Android 开发中,有两种主流的状态管理方法在 ViewModel 中被广泛使用。视频详细比较了这两种方法的优缺点和适用场景。
方法一:独立状态变量 (MVVM)
这种方法在 ViewModel 中为每个状态字段创建独立的 StateFlow:
// ViewModel A (MVVM 方式)
private val _email = MutableStateFlow("")
val email = _email.asStateFlow()
private val _password = MutableStateFlow("")
val password = _password.asStateFlow()
在这种方法下,屏幕上的每个状态都有自己的独立 StateFlow。
方法二:状态数据类 (MVI)
这种方法将所有屏幕状态捆绑在一个数据类中,ViewModel 只需暴露单一 StateFlow:
// ViewModel B (MVI 方式)
data class RegisterState(
val email: String = "",
val password: String = "",
val isEmailValid: Boolean = false,
val isPasswordValid: Boolean = false,
val canRegister: Boolean = false
)
private val _state = MutableStateFlow(RegisterState())
val state = _state.asStateFlow()
这种方法将所有与屏幕相关的状态组合成一个数据类,减少了需要在 ViewModel 中定义的字段数量。
两种方法的优势比较
UI 层面的优势
在 UI 层面,状态数据类方法(MVI 方式)有明显优势,尤其是在使用 Jetpack Compose 时:
当使用独立状态字段时,需要将每个状态单独传递给 UI 组件:
RegisterScreenA(
email = viewModel.email,
password = viewModel.password,
// 可能有20个甚至更多状态需要传递
)
而使用状态数据类方法时,只需传递单一状态对象:
RegisterScreenB(
state = viewModel.state
)
这在复杂屏幕上尤其明显 - 如果有20个不同的状态字段,使用独立状态方法需要传递20个参数,而使用状态数据类方法始终只需传递一个参数。
响应式编程的灵活性
独立状态字段方法(MVVM)在响应式编程方面提供了更大的灵活性:
// ViewModel A 中派生状态的简便方法
val isEmailValid = email.map { it.isValidEmail() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val isPasswordValid = password.map { it.isValidPassword() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val canRegister = combine(isEmailValid, isPasswordValid) { emailValid, passwordValid ->
emailValid && passwordValid
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
在这种方法中,你可以轻松组合多个状态流,并且这些派生状态只会在相关输入状态改变时更新。
而在状态数据类方法中,实现相同的响应式行为需要更多代码:
// ViewModel B 中需要手动设置监听和更新
init {
viewModelScope.launch {
_state.distinctUntilChangedBy { it.email }
.map { it.email.isValidEmail() }
.onEach { isEmailValid ->
_state.update { it.copy(isEmailValid = isEmailValid) }
}
.collect()
}
// 需要为每个派生状态添加类似代码
viewModelScope.launch {
_state.collect { state ->
_state.update {
it.copy(canRegister = state.isEmailValid && state.isPasswordValid)
}
}
}
}
在这种情况下,状态数据类方法需要更多样板代码,而且状态收集器会在每次状态变化时触发,而不仅仅是在相关字段变化时。
状态更新机制
两种方法的状态更新机制略有不同:
独立状态字段 (MVVM)
fun updateEmail(newEmail: String) {
_email.update { newEmail }
}
状态数据类 (MVI)
fun updateEmail(newEmail: String) {
_state.update { it.copy(email = newEmail) }
}
两种方法的代码量相似。重要的是,在使用状态数据类方法时,应始终使用 update 函数而不是直接赋值,以避免多线程环境中的竞态条件。
适用场景建议
XML 布局项目
在使用传统 XML 布局的项目中,MVVM 方法(独立状态字段)可能更合适,因为:
- 可以为每个 UI 元素单独设置监听器
- 只有当特定状态改变时,才会重新计算相应的 UI 元素
- 避免了整个 UI 因单一状态变化而重新计算
Jetpack Compose 项目
在 Jetpack Compose 项目中,MVI 方法(状态数据类)通常更优,因为:
- Compose 足够智能,能够只重组受影响的组件,即使使用单一状态对象
- 大大减少了将状态传递给 UI 组件的样板代码
- 与 sealed interface(用于用户操作)结合使用时,可以简化复杂屏幕的状态和事件处理
结论
没有一种方法在所有情况下都是最佳选择。两种方法各有优缺点:
- MVVM 方法(独立状态字段) 在响应式编程方面更灵活,对于 XML 项目可能更适合
- MVI 方法(状态数据类) 在 UI 层面上更简洁,特别适合 Jetpack Compose 项目