跳到主要内容

MVI 架构

问题

MVI(Model-View-Intent)架构的核心思想和实现方式是什么?

答案

MVI 单向数据流

核心概念

概念说明
Intent / Event用户操作或系统事件(输入)
State单一不可变状态(输出)
ReduceEvent + 旧 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

特性MVVMMVI
状态数量多个 StateFlow/LiveData单一 State
状态可变性各自独立修改不可变,通过 copy 实现
事件处理多个方法统一 onEvent()
时间旅行调试困难容易(记录 State 历史)
复杂度较低略高(但更规范)
选择建议
  • 简单页面:MVVM 足够
  • 复杂状态交互:MVI 更规范,状态可预测
  • 团队规模大:MVI 统一模式降低沟通成本

常见面试问题

Q1: MVI 的单一状态有什么优缺点?

答案

优点

  • 状态一致:不会出现 isLoading=true 同时 error!=null 的矛盾状态
  • 容易调试:每个状态变化都可追溯
  • Compose 友好:单一 State 直接映射 UI

缺点

  • 大 State 对象频繁 copy 可能有性能开销
  • 部分状态变化导致整个 UI 重组(Compose 通过 derivedStateOfkey 优化)

Q2: State 和 Effect 如何区分?

答案

  • State:持续性的 UI 状态,新订阅者需要知道当前状态(如列表数据、加载状态)
  • Effect:一次性事件,执行完就消失(如 Toast、导航、SnackBar)

StateFlow(replay=1)用于 State,SharedFlow(replay=0)用于 Effect。

相关链接