MVI 架构
问题
MVI(Model-View-Intent)架构的核心思想和实现方式是什么?
答案
MVI 单向数据流
核心概念:
| 概念 | 说明 |
|---|---|
| Intent / Event | 用户操作或系统事件(输入) |
| State | 单一不可变状态(输出) |
| Reduce | Event + 旧 State → 新 State |
| Side Effect | 一次性事件(不属于状态) |
MVI 完整实现
// 1. 定义 State、Event、Effect
data class UserListState(
val isLoading: Boolean = false,
val users: List<User> = emptyList(),
val error: String? = null
)
sealed class UserListEvent {
data object LoadUsers : UserListEvent()
data class SearchUsers(val query: String) : UserListEvent()
data class DeleteUser(val userId: String) : UserListEvent()
data object Refresh : UserListEvent()
}
sealed class UserListEffect {
data class ShowToast(val message: String) : UserListEffect()
data class NavigateToDetail(val userId: String) : UserListEffect()
}
// 2. ViewModel
class UserListViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow(UserListState())
val state: StateFlow<UserListState> = _state.asStateFlow()
private val _effect = MutableSharedFlow<UserListEffect>()
val effect: SharedFlow<UserListEffect> = _effect.asSharedFlow()
// 处理 Event
fun onEvent(event: UserListEvent) {
when (event) {
is UserListEvent.LoadUsers -> loadUsers()
is UserListEvent.SearchUsers -> searchUsers(event.query)
is UserListEvent.DeleteUser -> deleteUser(event.userId)
is UserListEvent.Refresh -> loadUsers()
}
}
private fun loadUsers() {
viewModelScope.launch {
// 更新状态:Loading
_state.update { it.copy(isLoading = true, error = null) }
try {
val users = repository.getUsers()
// 更新状态:Success
_state.update { it.copy(isLoading = false, users = users) }
} catch (e: Exception) {
// 更新状态:Error
_state.update { it.copy(isLoading = false, error = e.message) }
}
}
}
private fun deleteUser(userId: String) {
viewModelScope.launch {
try {
repository.deleteUser(userId)
_state.update { state ->
state.copy(users = state.users.filter { it.id != userId })
}
// 发送一次性事件
_effect.emit(UserListEffect.ShowToast("删除成功"))
} catch (e: Exception) {
_effect.emit(UserListEffect.ShowToast("删除失败"))
}
}
}
private fun searchUsers(query: String) { /* ... */ }
}
// 3. View (Compose)
@Composable
fun UserListScreen(viewModel: UserListViewModel = viewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
val context = LocalContext.current
// 收集一次性事件
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
is UserListEffect.ShowToast ->
Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
is UserListEffect.NavigateToDetail -> { /* 导航 */ }
}
}
}
// 根据 State 渲染 UI
when {
state.isLoading -> LoadingIndicator()
state.error != null -> ErrorView(
message = state.error!!,
onRetry = { viewModel.onEvent(UserListEvent.Refresh) }
)
else -> UserList(
users = state.users,
onDelete = { userId -> viewModel.onEvent(UserListEvent.DeleteUser(userId)) }
)
}
}
MVI vs MVVM
| 特性 | MVVM | MVI |
|---|---|---|
| 状态数量 | 多个 StateFlow/LiveData | 单一 State |
| 状态可变性 | 各自独立修改 | 不可变,通过 copy 实现 |
| 事件处理 | 多个方法 | 统一 onEvent() |
| 时间旅行调试 | 困难 | 容易(记录 State 历史) |
| 复杂度 | 较低 | 略高(但更规范) |
选择建议
- 简单页面:MVVM 足够
- 复杂状态交互:MVI 更规范,状态可预测
- 团队规模大:MVI 统一模式降低沟通成本
常见面试问题
Q1: MVI 的单一状态有什么优缺点?
答案:
优点:
- 状态一致:不会出现
isLoading=true同时error!=null的矛盾状态 - 容易调试:每个状态变化都可追溯
- Compose 友好:单一 State 直接映射 UI
缺点:
- 大 State 对象频繁 copy 可能有性能开销
- 部分状态变化导致整个 UI 重组(Compose 通过
derivedStateOf或key优化)
Q2: State 和 Effect 如何区分?
答案:
- State:持续性的 UI 状态,新订阅者需要知道当前状态(如列表数据、加载状态)
- Effect:一次性事件,执行完就消失(如 Toast、导航、SnackBar)
StateFlow(replay=1)用于 State,SharedFlow(replay=0)用于 Effect。