Android Jetpack Compose + MVI 开发流程深度分析

MVI 架构核心原理

MVI(Model-View-Intent)是一种基于单向数据流的架构模式,其核心组件关系如下:

scss 复制代码
[View] -- Intents --> [ViewModel] -- States --> [View]
  |                      |
用户交互事件             处理业务逻辑
  |                      |
[View] <-- Effects ---- [ViewModel]
       (一次性事件)

MVI 核心组件解析:

  1. Model (状态)

    • 不可变数据类表示UI的所有状态
    • 包含数据、加载状态、错误信息等
    • 使用Kotlin的data class确保状态不可变
  2. View (Compose UI)

    • 声明式UI组件
    • 职责:
      • 发送Intents(用户交互)
      • 渲染State
      • 处理Effects(导航、Toast等)
  3. Intent (用户意图)

    • 密封类表示所有可能的用户操作
    • 每个交互对应一个Intent子类
    • 示例:AddTodoIntent, DeleteTodoIntent
  4. 单向数据流

    graph LR A[用户操作] --> B[发送Intent] B --> C[ViewModel处理] C --> D[更新State] D --> E[UI渲染新State] C --> F[发送Effect] F --> G[UI处理Effect]

MVI 开发流程详解

  1. 状态定义

    • 创建包含所有UI状态的数据类
    • 使用密封类管理不同状态(Loading/Success/Error)
  2. Intent定义

    • 创建密封类表示所有用户操作
    • 每个操作对应一个Intent子类
  3. ViewModel实现

    • 使用StateFlow管理状态
    • 使用SharedFlow管理副作用(Effects)
    • 创建处理Intent的函数
    • 使用状态缩减器(Reducer)模式
  4. UI层实现

    • 使用collectAsState()观察状态
    • 使用LaunchedEffect收集Effects
    • 将用户操作包装为Intents发送
  5. 副作用处理

    • 导航事件
    • 显示Toast/Snackbar
    • 权限请求
    • 使用单独的数据流管理

Todo 应用 MVI 最佳实践

1. 添加依赖 (app/build.gradle)

gradle 复制代码
dependencies {
    // Compose
    implementation 'androidx.activity:activity-compose:1.8.0'
    implementation "androidx.compose.material3:material3:1.1.2"
    
    // Lifecycle
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2"
    implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
    
    // Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
    
    // Room
    implementation "androidx.room:room-runtime:2.6.0"
    implementation "androidx.room:room-ktx:2.6.0"
    kapt "androidx.room:room-compiler:2.6.0"
}

2. 状态定义

kotlin 复制代码
// 状态管理
data class TodoState(
    val todos: List<Todo> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null,
    val newTodoTitle: String = ""
) {
    // 派生状态
    val completedTodos: List<Todo>
        get() = todos.filter { it.isCompleted }
    
    val activeTodos: List<Todo>
        get() = todos.filterNot { it.isCompleted }
}

// 一次性事件(副作用)
sealed interface TodoEffect {
    data class ShowMessage(val message: String) : TodoEffect
    data class NavigateToDetail(val todoId: Long) : TodoEffect
}

3. Intent定义

kotlin 复制代码
// 用户意图
sealed interface TodoIntent {
    // 数据加载
    object LoadTodos : TodoIntent
    
    // 用户操作
    data class AddTodo(val title: String) : TodoIntent
    data class UpdateTodoTitle(val title: String) : TodoIntent
    data class ToggleTodoCompletion(val todoId: Long) : TodoIntent
    data class DeleteTodo(val todoId: Long) : TodoIntent
    data class EditTodo(val todo: Todo) : TodoIntent
    
    // UI事件
    object ShowAddDialog : TodoIntent
    object HideAddDialog : TodoIntent
}

4. ViewModel实现

kotlin 复制代码
class TodoViewModel(
    private val repository: TodoRepository
) : ViewModel() {
    
    // 状态管理
    private val _state = MutableStateFlow(TodoState(isLoading = true))
    val state: StateFlow<TodoState> = _state.asStateFlow()
    
    // 副作用管理
    private val _effects = MutableSharedFlow<TodoEffect>()
    val effects: SharedFlow<TodoEffect> = _effects.asSharedFlow()
    
    init {
        processIntent(TodoIntent.LoadTodos)
    }
    
    // 处理所有用户意图
    fun processIntent(intent: TodoIntent) {
        when (intent) {
            TodoIntent.LoadTodos -> loadTodos()
            is TodoIntent.AddTodo -> addTodo(intent.title)
            is TodoIntent.UpdateTodoTitle -> updateTitle(intent.title)
            is TodoIntent.ToggleTodoCompletion -> toggleCompletion(intent.todoId)
            is TodoIntent.DeleteTodo -> deleteTodo(intent.todoId)
            TodoIntent.ShowAddDialog -> showAddDialog()
            TodoIntent.HideAddDialog -> hideAddDialog()
            is TodoIntent.EditTodo -> editTodo(intent.todo)
        }
    }
    
    private fun loadTodos() {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true, error = null) }
            
            repository.getTodos()
                .catch { e ->
                    _state.update { it.copy(error = e.message, isLoading = false) }
                    _effects.emit(TodoEffect.ShowMessage("加载失败: ${e.message}"))
                }
                .collect { todos ->
                    _state.update { it.copy(todos = todos, isLoading = false) }
                }
        }
    }
    
    private fun addTodo(title: String) {
        if (title.isBlank()) {
            viewModelScope.launch {
                _effects.emit(TodoEffect.ShowMessage("标题不能为空"))
            }
            return
        }
        
        viewModelScope.launch {
            val newTodo = Todo(title = title)
            repository.addTodo(newTodo)
            _state.update { 
                it.copy(newTodoTitle = "", todos = it.todos + newTodo) 
            }
            _effects.emit(TodoEffect.ShowMessage("添加成功"))
        }
    }
    
    private fun toggleCompletion(todoId: Long) {
        viewModelScope.launch {
            val todo = _state.value.todos.find { it.id == todoId } ?: return@launch
            val updated = todo.copy(isCompleted = !todo.isCompleted)
            repository.updateTodo(updated)
            
            _state.update { state ->
                state.copy(todos = state.todos.map {
                    if (it.id == todoId) updated else it
                })
            }
        }
    }
    
    private fun updateTitle(title: String) {
        _state.update { it.copy(newTodoTitle = title) }
    }
    
    private fun showAddDialog() {
        _state.update { it.copy(showAddDialog = true) }
    }
    
    private fun hideAddDialog() {
        _state.update { it.copy(showAddDialog = false) }
    }
    
    private fun deleteTodo(todoId: Long) {
        viewModelScope.launch {
            val todo = _state.value.todos.find { it.id == todoId } ?: return@launch
            repository.deleteTodo(todo)
            
            _state.update { state ->
                state.copy(todos = state.todos.filterNot { it.id == todoId })
            }
            
            _effects.emit(TodoEffect.ShowMessage("已删除: ${todo.title}"))
        }
    }
    
    private fun editTodo(todo: Todo) {
        viewModelScope.launch {
            repository.updateTodo(todo)
            _state.update { state ->
                state.copy(todos = state.todos.map {
                    if (it.id == todo.id) todo else it
                })
            }
            _effects.emit(TodoEffect.ShowMessage("更新成功"))
        }
    }
}

5. Compose UI实现

kotlin 复制代码
@Composable
fun TodoScreen(
    viewModel: TodoViewModel = viewModel()
) {
    val state by viewModel.state.collectAsState()
    
    // 收集副作用
    val context = LocalContext.current
    LaunchedEffect(Unit) {
        viewModel.effects.collect { effect ->
            when (effect) {
                is TodoEffect.ShowMessage -> {
                    Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
                }
                is TodoEffect.NavigateToDetail -> {
                    // 处理导航
                }
            }
        }
    }
    
    TodoScreenContent(
        state = state,
        onIntent = viewModel::processIntent
    )
}

@Composable
private fun TodoScreenContent(
    state: TodoState,
    onIntent: (TodoIntent) -> Unit
) {
    Scaffold(
        topBar = { TopAppBar(title = { Text("Todo MVI") }) },
        floatingActionButton = {
            FloatingActionButton(
                onClick = { onIntent(TodoIntent.ShowAddDialog) }
            ) {
                Icon(Icons.Filled.Add, "添加")
            }
        }
    ) { padding ->
        when {
            state.isLoading -> LoadingView()
            state.error != null -> ErrorView(state.error, onIntent)
            else -> TodoListView(state, onIntent, Modifier.padding(padding))
        }
        
        // 添加对话框
        if (state.showAddDialog) {
            AddTodoDialog(
                title = state.newTodoTitle,
                onTitleChange = { onIntent(TodoIntent.UpdateTodoTitle(it)) },
                onDismiss = { onIntent(TodoIntent.HideAddDialog) },
                onConfirm = { onIntent(TodoIntent.AddTodo(state.newTodoTitle)) }
            )
        }
    }
}

@Composable
fun TodoListView(
    state: TodoState,
    onIntent: (TodoIntent) -> Unit,
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier) {
        // 活动任务
        Text("待完成 (${state.activeTodos.size})", style = MaterialTheme.typography.titleMedium)
        LazyColumn {
            items(state.activeTodos, key = { it.id }) { todo ->
                TodoItem(
                    todo = todo,
                    onToggle = { onIntent(TodoIntent.ToggleTodoCompletion(todo.id)) },
                    onDelete = { onIntent(TodoIntent.DeleteTodo(todo.id)) },
                    onEdit = { onIntent(TodoIntent.EditTodo(it)) }
                )
            }
        }
        
        // 已完成任务
        if (state.completedTodos.isNotEmpty()) {
            Text("已完成 (${state.completedTodos.size})", style = MaterialTheme.typography.titleMedium)
            LazyColumn {
                items(state.completedTodos, key = { it.id }) { todo ->
                    TodoItem(
                        todo = todo,
                        onToggle = { onIntent(TodoIntent.ToggleTodoCompletion(todo.id)) },
                        onDelete = { onIntent(TodoIntent.DeleteTodo(todo.id)) },
                        onEdit = { onIntent(TodoIntent.EditTodo(it)) }
                    )
                }
            }
        }
    }
}

@Composable
fun TodoItem(
    todo: Todo,
    onToggle: () -> Unit,
    onDelete: () -> Unit,
    onEdit: (Todo) -> Unit
) {
    var showEditDialog by remember { mutableStateOf(false) }
    
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(16.dp)
        ) {
            Checkbox(
                checked = todo.isCompleted,
                onCheckedChange = { onToggle() }
            )
            
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = todo.title,
                    style = MaterialTheme.typography.titleMedium,
                    textDecoration = if (todo.isCompleted) LineThrough else None
                )
                if (todo.description.isNotBlank()) {
                    Text(text = todo.description, style = MaterialTheme.typography.bodySmall)
                }
            }
            
            IconButton(onClick = { showEditDialog = true }) {
                Icon(Icons.Default.Edit, "编辑")
            }
            
            IconButton(onClick = onDelete) {
                Icon(Icons.Default.Delete, "删除")
            }
        }
    }
    
    if (showEditDialog) {
        EditTodoDialog(
            todo = todo,
            onDismiss = { showEditDialog = false },
            onSave = { updated ->
                onEdit(updated)
                showEditDialog = false
            }
        )
    }
}

@Composable
fun EditTodoDialog(
    todo: Todo,
    onDismiss: () -> Unit,
    onSave: (Todo) -> Unit
) {
    var title by remember { mutableStateOf(todo.title) }
    var description by remember { mutableStateOf(todo.description) }
    
    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text("编辑任务") },
        text = {
            Column {
                TextField(
                    value = title,
                    onValueChange = { title = it },
                    label = { Text("标题") }
                )
                Spacer(Modifier.height(8.dp))
                TextField(
                    value = description,
                    onValueChange = { description = it },
                    label = { Text("描述") }
                )
            }
        },
        confirmButton = {
            Button(
                onClick = { 
                    onSave(todo.copy(title = title, description = description))
                }
            ) {
                Text("保存")
            }
        },
        dismissButton = {
            Button(onClick = onDismiss) {
                Text("取消")
            }
        }
    )
}

MVI 最佳实践要点

  1. 严格单向数据流

    • UI只能发送Intents,不能直接修改状态
    • ViewModel是唯一能修改状态的地方
    • 使用copy()确保状态不可变
  2. 状态设计原则

    • 包含UI所需全部数据
    • 使用派生属性减少冗余
    • 区分持久状态和临时状态
  3. 副作用处理

    graph TD A[ViewModel] -->|发送| B[Effect] B --> C[UI层通过LaunchedEffect收集] C --> D[执行一次性操作]
  4. 测试策略

    • ViewModel测试

      kotlin 复制代码
      @Test
      fun `添加任务应更新状态`() = runTest {
          val vm = TodoViewModel(mockRepository)
          vm.processIntent(TodoIntent.AddTodo("New Task"))
          
          val state = vm.state.value
          assertEquals(1, state.todos.size)
          assertEquals("New Task", state.todos[0].title)
      }
    • UI测试

      kotlin 复制代码
      composeTestRule.onNodeWithText("添加").performClick()
      composeTestRule.onNodeWithText("标题").performTextInput("New Task")
      composeTestRule.onNodeWithText("确认").performClick()
      composeTestRule.onNodeWithText("New Task").assertExists()
  5. 性能优化

    • 使用key参数优化LazyColumn重组
    • 使用derivedStateOf处理复杂状态计算
    • 避免在状态中包含大对象
  6. 高级模式

    • Reducer模式

      kotlin 复制代码
      fun reduce(state: TodoState, intent: TodoIntent): TodoState {
          return when (intent) {
              is AddTodo -> state.copy(todos = state.todos + intent.todo)
              // 其他状态转换...
          }
      }
    • 中间件:处理日志、分析等横切关注点

此MVI实现通过严格的状态管理和单向数据流,提供了比MVVM更高的可预测性和可测试性。每个用户操作都明确表示为Intent,状态变化完全可追踪,特别适合复杂业务场景。

相关推荐
峥嵘life32 分钟前
Android14 锁屏密码修改为至少6位
android·安全
2501_915918418 小时前
iOS WebView 调试实战 localStorage 与 sessionStorage 同步问题全流程排查
android·ios·小程序·https·uni-app·iphone·webview
Digitally9 小时前
如何永久删除安卓设备中的照片(已验证)
android·gitee
hmywillstronger10 小时前
【Settlement】P1:整理GH中的矩形GRID角点到EXCEL中
android·excel
lvronglee10 小时前
如何编译RustDesk(Unbuntu 和Android版本)
android·rustdesk
byadom_IT11 小时前
【Android】Popup menu:弹出式菜单
android
pk_xz12345612 小时前
基于WebSockets和OpenCV的安卓眼镜视频流GPU硬解码实现
android·人工智能·opencv
安卓开发者13 小时前
Android KTX:让Kotlin开发更简洁高效的利器
android·开发语言·kotlin
whysqwhw13 小时前
OkHttp WebSocket 实现详解:数据传输、帧结构与组件关系
android
阿华的代码王国13 小时前
【Android】xml和Java两种方式实现发送邮件页面
android·xml·java