MVI架构3--实战示例:我的收藏页面

这个例子展示一个简单的

"我的收藏"页面:包含加载数据、点击收藏(更新状态)和弹出提醒(副作用)。

1. 定义三要素(MVI 核心)

复制代码
// 1. UiState: 页面长什么样?(持久的)
data class MineUiState(
    val isLoading: Boolean = false,
    val items: List<String> = emptyList(),
    val isFavorite: Boolean = false
) : UiState

// 2. Intent: 用户想干什么?(输入)
sealed class MineIntent : Intent {
    object FetchData : MineIntent()
    object ToggleFavorite : MineIntent()
}

// 3. UiEffect: 发生一次就消失的动作(输出)
sealed class MineUiEffect : UiEffect {
    data class ShowToast(val message: String) : MineUiEffect()
}

2. ViewModel 实现(逻辑大脑)

复制代码
class MineViewModel : SimpleViewModel<MineUiState, MineIntent, MineUiEffect>() {

    override fun createInitialState() = MineUiState()

    override suspend fun handleIntent(intent: MineIntent) {
        when (intent) {
            is MineIntent.FetchData -> {
                setState { copy(isLoading = true) }
                delay(1000) // 模拟网络请求
                setState { copy(isLoading = false, items = listOf("歌单1", "歌单2")) }
            }
            is MineIntent.ToggleFavorite -> {
                val newState = !currentState.isFavorite
                setState { copy(isFavorite = newState) }
                
                // 发送一次性副作用
                val msg = if (newState) "已添加到收藏" else "已取消收藏"
                sendEffect(MineUiEffect.ShowToast(msg))
            }
        }
    }
}

3. Compose UI 实现(渲染与消费)

复制代码
@Composable
fun MineScreen(viewModel: MineViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    val context = LocalContext.current

    // 【核心】监听 UiEffect:这里只会在 Effect 发送时执行一次
    LaunchedEffect(Unit) {
        viewModel.uiEffect.collect { effect ->
            when (effect) {
                is MineUiEffect.ShowToast -> {
                    Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    // 布局渲染
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        if (uiState.isLoading) {
            CircularProgressIndicator()
        } else {
            // 根据 UiState 显示红心颜色
            Icon(
                imageVector = if (uiState.isFavorite) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
                contentDescription = null,
                modifier = Modifier.clickable { 
                    // 发送 Intent
                    viewModel.sendIntent(MineIntent.ToggleFavorite) 
                },
                tint = if (uiState.isFavorite) Color.Red else Color.Gray
            )
            
            uiState.items.forEach { Text(text = it) }
        }

        Button(onClick = { viewModel.sendIntent(MineIntent.FetchData) }) {
            Text("刷新数据")
        }
    }
}

为什么这样设计

  1. 旋转屏幕测试:当你点击收藏弹出 Toast 后旋转屏幕,界面会依据 uiState.isFavorite 保持红心状态,但 Toast 不会再弹出来,因为 LaunchedEffect 里的 collect 只响应新发射的 Effect。
  2. 代码位置:所有的 if-else 逻辑都在 ViewModel 里的 handleIntent,Compose 页面非常干净。
  3. 单向流动:UI (Intent) -> ViewModel (Logic) -> UI (State/Effect)。
相关推荐
CappuccinoRose8 小时前
数据设计 - 软考备战(五十)
软考·设计规范·er图·数据设计·规范化理论
儿歌八万首2 天前
Jetpack Compose 实战:实现一个动态平滑折线图
android·折线图·compose
stevenzqzq2 天前
compose中Modifier.padding 与 contentPadding 区别
compose
儿歌八万首3 天前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
鸽芷咕3 天前
KingbaseES数据库设计规范与SQL开发最佳实践
数据库·sql·设计规范
薛定猫AI5 天前
【深度解析】Claude Code Skills 工作流:用知识图谱、设计规范与 Agent 工具链提升 AI 编程效率
人工智能·知识图谱·设计规范
cui17875687 天前
排队免单模式:从爆火到优化,探寻实体商业新出路
大数据·人工智能·设计模式·个人开发·设计规范
电子科技圈10 天前
IAR作为Qt Group独立BU携两项重磅汽车电子应用开发方案首秀北京车展
开发语言·人工智能·汽车·软件工程·软件构建·代码规范·设计规范
儿歌八万首10 天前
Compose 自定义组件:封装一个通用标题栏
android·compose·标题栏
xiaohuoji12910 天前
量化交易系统架构设计:从回测到实盘的4层分层方案(附架构图)
大数据·人工智能·数据挖掘·数据分析·个人开发·设计规范