这个例子展示一个简单的
"我的收藏"页面:包含加载数据、点击收藏(更新状态)和弹出提醒(副作用)。
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("刷新数据")
}
}
}
为什么这样设计
- 旋转屏幕测试:当你点击收藏弹出 Toast 后旋转屏幕,界面会依据 uiState.isFavorite 保持红心状态,但 Toast 不会再弹出来,因为 LaunchedEffect 里的 collect 只响应新发射的 Effect。
- 代码位置:所有的 if-else 逻辑都在 ViewModel 里的 handleIntent,Compose 页面非常干净。
- 单向流动:UI (Intent) -> ViewModel (Logic) -> UI (State/Effect)。