MVI 核心概念
MVI(Model-View-Intent)是一种响应式单向数据流架构模式,由三个核心组件构成:
- Model:代表应用状态(不可变数据)
- View:反映状态的 UI(Activity/Fragment/Composable)
- Intent:用户/系统触发的动作(非 Android 的 Intent)
Android 最佳实践实现方案
// 1. 状态定义 (Model)
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null, // 使用可空类型避免状态污染
val isSuccess: Boolean = false
)
// 2. 密封类定义 Intent(用户意图)
sealed class LoginIntent {
data class EmailChanged(val email: String) : LoginIntent()
data class PasswordChanged(val password: String) : LoginIntent()
object Submit : LoginIntent()
object ResetError : LoginIntent()
}
// 3. ViewModel 处理业务逻辑
class LoginViewModel : ViewModel() {
private val _state = MutableStateFlow(LoginState())
val state: StateFlow<LoginState> = _state.asStateFlow()
private val _oneTimeEvents = Channel<LoginEvent>() // 处理一次性事件(如导航)
val oneTimeEvents = _oneTimeEvents.receiveAsFlow()
fun processIntent(intent: LoginIntent) {
when (intent) {
is LoginIntent.EmailChanged -> updateEmail(intent.email)
is LoginIntent.PasswordChanged -> updatePassword(intent.password)
LoginIntent.Submit -> submitLogin()
LoginIntent.ResetError -> resetError()
}
}
private fun updateEmail(email: String) {
_state.update { it.copy(email = email) } // 使用 copy 保持不可变性
}
private fun submitLogin() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
// 模拟网络请求
val result = authRepository.login(
_state.value.email,
_state.value.password
)
result.fold(
onSuccess = {
_state.update { it.copy(isSuccess = true, isLoading = false) }
_oneTimeEvents.send(LoginEvent.NavigateToHome)
},
onFailure = {
_state.update { it.copy(
error = it.message ?: "Unknown error",
isLoading = false
)}
}
)
}
}
}
// 4. 一次性事件处理(副作用)
sealed class LoginEvent {
object NavigateToHome : LoginEvent()
data class ShowToast(val message: String) : LoginEvent()
}
关键实践原则
单向数据流
User Interaction → Intent → ViewModel → State Update → UI Render
不可变状态
- 使用 Kotlin
data class
+copy()
更新状态 - 禁止直接修改状态属性(保证线程安全)
- 使用 Kotlin
副作用分离
- 使用
Channel
/SharedFlow
处理导航/Toast 等一次性事件 - 避免在状态中包含瞬时数据(如 Toast 消息)
- 使用
状态可持久化
// 使用 SavedStateHandle 恢复状态 init { _state.value = savedStateHandle.get("loginState") ?: LoginState() } // 状态变更时保存 _state .onEach { savedStateHandle["loginState"] = it } .launchIn(viewModelScope)
响应式 UI(Compose 最佳实践)
@Composable fun LoginScreen(viewModel: LoginViewModel) { val state by viewModel.state.collectAsState() val coroutineScope = rememberCoroutineScope() LaunchedEffect(Unit) { viewModel.oneTimeEvents.collect { event -> when (event) { LoginEvent.NavigateToHome -> navController.navigate("home") is LoginEvent.ShowToast -> Toast.makeText(context, event.message).show() } } } Column { TextField( value = state.email, onValueChange = { viewModel.processIntent(LoginIntent.EmailChanged(it)) } ) Button(onClick = { viewModel.processIntent(LoginIntent.Submit) }) { if (state.isLoading) CircularProgressIndicator() else Text("Login") } state.error?.let { ErrorText(it) } } }
优势与适用场景
优势 | 适用场景 |
---|---|
状态可预测:单一数据源保证 UI 一致性 | 复杂表单交互 |
易调试:状态快照可持久化/回放 | 实时数据仪表盘 |
强隔离性:业务逻辑与 UI 解耦 | 多步骤流程(如支付) |
天然防抖:状态合并优化性能 | 需要离线缓存的场景 |
性能优化技巧
状态去重
// 使用 distinctUntilChanged 避免无效更新 _state .distinctUntilChanged { old, new -> old.email == new.email && old.isLoading == new.isLoading } .collect { ... }
Intent 合并
// 使用 buffer() 处理快速连续点击 intents .buffer(Channel.UNLIMITED) .collect { processIntent(it) }
状态分片 (大型项目)
// 按功能模块拆分状态 data class AppState( val userState: UserState, val settingsState: SettingsState )
测试策略
@Test
fun `email update should modify state`() = runTest {
// Given
val viewModel = LoginViewModel()
// When
viewModel.processIntent(LoginIntent.EmailChanged("test@example.com"))
// Then
assertEquals("test@example.com", viewModel.state.value.email)
}
@Test
fun `login should emit navigation event on success`() = runTest {
// Given
val fakeRepo = FakeAuthRepo(shouldSucceed = true)
val viewModel = LoginViewModel(repo = fakeRepo)
// When
viewModel.processIntent(LoginIntent.Submit)
// Then
val event = viewModel.oneTimeEvents.receive()
assertTrue(event is LoginEvent.NavigateToHome)
}
与 MVVM 的关键区别
特性 | MVI | 传统 MVVM |
---|---|---|
数据流向 | 严格单向 | 双向绑定 |
状态管理 | 单一不可变状态对象 | 多个 LiveData 分散管理 |
UI 更新机制 | 状态驱动 (State → UI) | 事件驱动 (Event → UI) |
调试能力 | 时间旅行调试 | 困难 |
推荐工具链
- 状态管理:
StateFlow
(Jetpack),MutableState
(Compose) - 副作用处理:
Channel
,SharedFlow
- 依赖注入:Hilt
- 异步操作:
coroutines
+Flow
- 测试:
kotlinx-coroutines-test
,Turbine
最佳实践总结:MVI 通过强制单向数据流和不可变状态,解决了 Android 开发中常见的状态同步问题。结合 Kotlin 协程和 Jetpack 组件,可构建高可维护性、可测试性的应用,特别适合复杂交互场景。