Android MVI架构模式详解

发布于:2025-07-01 ⋅ 阅读:(21) ⋅ 点赞:(0)
MVI 核心概念

MVI(Model-View-Intent)是一种响应式单向数据流架构模式,由三个核心组件构成:

  1. Model:代表应用状态(不可变数据)
  2. View:反映状态的 UI(Activity/Fragment/Composable)
  3. 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()
}

关键实践原则
  1. 单向数据流

    User Interaction → Intent → ViewModel → State Update → UI Render
    
  2. 不可变状态

    • 使用 Kotlin data class + copy() 更新状态
    • 禁止直接修改状态属性(保证线程安全)
  3. 副作用分离

    • 使用 Channel/SharedFlow 处理导航/Toast 等一次性事件
    • 避免在状态中包含瞬时数据(如 Toast 消息)
  4. 状态可持久化

    // 使用 SavedStateHandle 恢复状态
    init {
        _state.value = savedStateHandle.get("loginState") ?: LoginState()
    }
    
    // 状态变更时保存
    _state
        .onEach { savedStateHandle["loginState"] = it }
        .launchIn(viewModelScope)
    
  5. 响应式 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 解耦 多步骤流程(如支付)
天然防抖:状态合并优化性能 需要离线缓存的场景

性能优化技巧
  1. 状态去重

    // 使用 distinctUntilChanged 避免无效更新
    _state
      .distinctUntilChanged { old, new ->
          old.email == new.email && old.isLoading == new.isLoading
      }
      .collect { ... }
    
  2. Intent 合并

    // 使用 buffer() 处理快速连续点击
    intents
      .buffer(Channel.UNLIMITED)
      .collect { processIntent(it) }
    
  3. 状态分片 (大型项目)

    // 按功能模块拆分状态
    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 组件,可构建高可维护性、可测试性的应用,特别适合复杂交互场景。