Android Kotlin MVVM与MVI组合场景(多MVI应用场景详解)
一、MVI架构核心设计思想(精简版)
MVI(Model-View-Intent)核心是三大原则+数据流思维,适配Android Kotlin开发的核心要点:以“唯一可信数据源”为基础,通过“单向数据流”(View→Intent→ViewModel→State→View)流转,依托Kotlin Flow(StateFlow/SharedFlow)实现响应式编程,严格区分State(持久化状态)与Event(一次性事件),解决状态混乱、逻辑不可追溯问题。
核心适配点:Kotlin的协程+Flow是MVI落地的核心技术,与MVVM的ViewModel天然契合,无需额外引入复杂组件,可无缝融入现有MVVM架构。
二、Android Kotlin MVVM与MVI组合核心架构(通用模板)
组合架构的核心逻辑:MVVM搭分层框架,MVI管数据流与状态,全程基于Kotlin语法实现,结构清晰且可直接落地,具体分层如下:
-
View层(MVVM) :Activity/Fragment/Compose,作为数据流的生产者(发送Intent)和消费者(观察State/Event),不处理业务逻辑,仅负责界面渲染和用户交互。
-
ViewModel层(MVVM+MVI核心) :持有UI State和Event,接收View发送的Intent,通过Reducer处理业务逻辑、转换State,借助Kotlin Flow推送状态/事件,同时承担MVVM中生命周期感知、数据持久化的职责。
-
Model层(MVVM) :Repository+DataSource,负责网络请求、本地数据库操作(Room),提供数据来源,通过Flow将数据反馈给ViewModel,与MVI的数据流逻辑无缝衔接。
-
MVI核心组件(嵌入ViewModel) :
- Intent:密封类(sealed class),封装所有用户操作(如点击、输入、下拉刷新);
- State:数据类(data class),聚合当前页面所有持久化状态(如加载状态、列表数据、输入内容);
- Event:密封类,封装一次性操作(如弹窗、页面跳转、吐司),用SharedFlow(replay=0)发送;
- Reducer:纯函数,接收当前State和Intent,返回新的State,保证状态更新可预测。
组合优势:既保留MVVM的生命周期管理、分层解耦能力,又借助MVI解决复杂页面的状态混乱问题,同时贴合Kotlin的响应式编程特性,减少模板代码、提升可维护性。
三、Android Kotlin MVVM+MVI多场景实战(重点)
以下场景均基于Kotlin+Flow+ViewModel实现,覆盖日常开发高频场景,每个场景包含“组合逻辑+核心代码片段+场景适配要点”,可直接复用。
场景1:复杂表单页面(最典型MVI应用场景)
适用场景:登录表单、注册表单、个人信息编辑页(多输入框、多校验规则、多按钮交互,状态联动性强)。
组合逻辑
MVVM:View层(Compose/XML)负责表单渲染,ViewModel层管理表单数据;MVI:用Intent封装输入、校验、提交操作,用State聚合所有表单状态(输入内容、校验结果、按钮状态、加载状态),用Event处理提交成功/失败的一次性提示。
核心代码片段(Kotlin)
// 1. MVI Intent(封装表单操作)
sealed class LoginIntent {
data class InputAccount(val account: String) : LoginIntent()
data class InputPassword(val password: String) : LoginIntent()
object CheckForm : LoginIntent()
object SubmitLogin : LoginIntent()
}
// 2. MVI State(聚合表单所有状态)
data class LoginState(
val account: String = "",
val password: String = "",
val accountValid: Boolean? = null, // 校验结果:null未校验,true通过,false失败
val passwordValid: Boolean? = null,
val isSubmitEnabled: Boolean = false, // 提交按钮是否可点击
val isLoading: Boolean = false, // 加载状态
val errorMsg: String? = null // 错误提示(持久化,可恢复)
)
// 3. MVI Event(一次性事件)
sealed class LoginEvent {
data class ShowToast(val msg: String) : LoginEvent()
object NavigateToHome : LoginEvent() // 登录成功跳转
}
// 4. ViewModel(MVVM+MVI结合)
class LoginViewModel : ViewModel() {
// 唯一可信数据源:StateFlow存储State
private val _uiState = MutableStateFlow(LoginState())
val uiState: StateFlow<LoginState> = _uiState.asStateFlow()
// 一次性事件:SharedFlow(非粘性)
private val _uiEvent = MutableSharedFlow<LoginEvent>()
val uiEvent: SharedFlow<LoginEvent> = _uiEvent.asSharedFlow()
// 接收Intent,通过Reducer处理状态
fun handleIntent(intent: LoginIntent) {
viewModelScope.launch {
_uiState.value = reduce(_uiState.value, intent)
}
}
// Reducer:纯函数,处理状态更新
private fun reduce(state: LoginState, intent: LoginIntent): LoginState {
return when (intent) {
is LoginIntent.InputAccount -> {
state.copy(account = intent.account)
}
is LoginIntent.InputPassword -> {
state.copy(password = intent.password)
}
LoginIntent.CheckForm -> {
val accountValid = state.account.isNotBlank()
val passwordValid = state.password.length >= 6
state.copy(
accountValid = accountValid,
passwordValid = passwordValid,
isSubmitEnabled = accountValid && passwordValid
)
}
LoginIntent.SubmitLogin -> {
// 模拟网络请求
viewModelScope.launch {
_uiState.value = state.copy(isLoading = true)
delay(1000)
if (state.account == "admin" && state.password == "123456") {
_uiEvent.emit(LoginEvent.NavigateToHome)
} else {
_uiEvent.emit(LoginEvent.ShowToast("账号密码错误"))
_uiState.value = state.copy(isLoading = false, errorMsg = "账号密码错误")
}
}
state.copy(isLoading = true)
}
}
}
}
// 5. View层(Compose示例,XML同理)
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event ->
when (event) {
is LoginEvent.ShowToast -> Toast.makeText(context, event.msg, Toast.LENGTH_SHORT).show()
LoginEvent.NavigateToHome -> navController.navigate("home")
}
}
}
// 表单渲染,点击/输入时发送Intent
Column {
TextField(
value = uiState.account,
onValueChange = { viewModel.handleIntent(LoginIntent.InputAccount(it)) },
label = { Text("账号") },
isError = uiState.accountValid == false
)
TextField(
value = uiState.password,
onValueChange = { viewModel.handleIntent(LoginIntent.InputPassword(it)) },
label = { Text("密码") },
isError = uiState.passwordValid == false
)
Button(
onClick = { viewModel.handleIntent(LoginIntent.CheckForm) },
modifier = Modifier.padding(vertical = 8.dp)
) {
Text("校验表单")
}
Button(
onClick = { viewModel.handleIntent(LoginIntent.SubmitLogin) },
enabled = uiState.isSubmitEnabled && !uiState.isLoading,
modifier = Modifier.padding(vertical = 8.dp)
) {
if (uiState.isLoading) CircularProgressIndicator(size = 20.dp)
else Text("登录")
}
}
}
场景适配要点
- 表单状态(输入内容、校验结果)用State存储,支持页面重建后恢复;
- 提交按钮状态由State联动控制,避免手动修改按钮可点击性,减少状态不一致;
- 登录结果(跳转、吐司)用Event处理,避免页面重建后重复触发(非粘性SharedFlow保证);
- Reducer纯函数设计,所有状态更新都集中在一个方法,便于调试和维护。
场景2:下拉刷新+上拉加载列表页
适用场景:商品列表、消息列表、数据列表(多状态:加载中、空数据、异常、分页加载,交互频繁)。
组合逻辑
MVVM:View层负责列表渲染(RecyclerView/Compose LazyColumn),ViewModel层调用Repository获取数据;MVI:用Intent封装下拉刷新、上拉加载、重试操作,用State聚合列表所有状态(列表数据、加载状态、空状态、异常状态、分页参数),用Event处理加载失败提示。
核心适配亮点
- 用StateFlow存储列表State,分页参数(当前页、页大小)也纳入State,保证分页逻辑连贯;
- 下拉刷新、上拉加载的加载状态分开管理(如isRefreshing、isLoadingMore),避免状态冲突;
- 空数据、异常状态纳入State,View层根据State自动渲染对应界面,无需手动判断;
- 借助Kotlin Flow的combine操作符,合并分页数据,实现列表无缝加载。
场景3:直播间交互页面(高频交互场景)
适用场景:直播带货、聊天直播间(高频操作:送礼、点赞、评论、切换镜头,多状态实时联动)。
组合逻辑
MVVM:View层负责直播画面、互动控件渲染,ViewModel层处理直播相关业务(送礼、点赞接口);MVI:用Intent封装所有互动操作(送礼、点赞、发送评论、切换镜头),用State聚合直播状态(观众数、点赞数、礼物列表、当前镜头、互动提示),用Event处理送礼成功、评论发送结果等一次性通知。
核心适配亮点
- 高频操作(点赞)用Intent批量处理,借助Flow的节流操作符(throttleFirst)避免接口频繁调用;
- 实时变化的状态(观众数、点赞数)用StateFlow实时推送,View层实时刷新,保证交互流畅;
- 送礼、评论等操作结果用Event处理,避免重复提示,同时支持异常重试(重新发送Intent);
- 镜头切换状态纳入State,页面重建后可恢复当前镜头,提升用户体验。
场景4:老项目MVP重构(增量改造场景)
适用场景:原有Kotlin MVP老模块,不想彻底推翻重构,需解决状态混乱、bug难排查问题。
组合逻辑
保留MVP的Presenter分层(暂不替换为ViewModel),在Presenter中嵌入MVI逻辑:用Intent封装View的所有操作,用State聚合界面状态,用Flow实现状态推送,逐步替换原有Presenter中的回调逻辑,实现增量改造。
核心适配亮点
- 不推翻原有MVP结构,仅在Presenter中引入MVI的State、Intent、Reducer,降低重构成本;
- 用State替换原有分散的变量(如isLoading、dataList),实现唯一可信数据源;
- 用Intent统一接收View的操作,替代原有Presenter中的多个方法调用,简化交互逻辑;
- 后续可逐步将Presenter替换为ViewModel,实现向MVVM+MVI的完整过渡。
场景5:全局状态管理(跨页面共享场景)
适用场景:登录态、主题切换、全局配置(跨Activity/Fragment共享状态,多页面联动)。
组合逻辑
MVVM:用单例ViewModel(或Hilt注入)作为全局状态持有者;MVI:用Intent封装全局操作(登录、登出、切换主题),用State聚合全局状态(登录信息、当前主题、全局配置),用Event处理全局通知(如登录过期提示),所有页面观察全局State,实现状态共享。
核心适配亮点
- 用单例ViewModel+StateFlow实现全局状态共享,保证所有页面获取的状态一致;
- 登录态、主题等核心状态纳入State,页面重建后自动恢复,无需手动传递数据;
- 全局操作(如登出)通过发送Intent触发,所有依赖该状态的页面自动刷新,避免手动通知;
- 借助Kotlin Flow的distinctUntilChanged操作符,避免无意义的状态刷新,提升性能。
场景6:筛选页(多条件联动场景)
适用场景:商品筛选、数据筛选(多筛选条件:下拉选择、输入框、开关,筛选条件联动,实时刷新结果)。
组合逻辑
MVVM:View层负责筛选条件渲染、筛选结果展示,ViewModel层调用筛选接口;MVI:用Intent封装所有筛选操作(选择条件、输入筛选关键词、重置筛选、确认筛选),用State聚合筛选条件、筛选结果、加载状态,用Event处理筛选异常提示。
核心适配亮点
- 筛选条件全部纳入State,联动逻辑通过Reducer处理,避免多个条件修改导致的状态混乱;
- 实时筛选(输入关键词即刷新)借助Flow的debounce操作符,避免接口频繁调用;
- 筛选结果、加载状态、空状态纳入State,View层根据State自动渲染,简化逻辑;
- 重置筛选只需发送一个Intent,Reducer重置所有筛选条件和结果,操作简洁。
四、MVVM+MVI组合开发注意事项(Kotlin环境)
- State设计:遵循“最小粒度”原则,避免State过大成为“上帝类”,复杂页面可按业务模块拆分多个State;
- Event处理:必须用非粘性SharedFlow(replay=0)或事件包装类(OneShotEvent),避免页面重建后重复触发;
- Flow使用:优先用StateFlow存储State(支持状态恢复),SharedFlow存储Event(一次性),结合协程保证线程安全;
- Reducer设计:必须是纯函数(无副作用、输入相同输出相同),所有State更新都集中在Reducer,便于调试;
- 场景适配:简单页面(如详情页)可直接用MVVM,无需强行引入MVI;复杂交互页面必须用MVI管状态,避免状态混乱;
- 性能优化:用distinctUntilChanged过滤重复State,用debounce/throttle处理高频操作,避免无意义的界面刷新。
五、总结
在Android Kotlin开发中,MVVM+MVI的组合是最贴合实际项目的架构方案——MVVM解决分层解耦、生命周期管理问题,MVI解决复杂页面的状态混乱、逻辑不可追溯问题,两者结合可覆盖从简单页面到复杂交互、从新项目开发到老项目重构的所有场景。
核心关键:基于Kotlin Flow实现MVI的响应式数据流,将MVI的核心组件(Intent/State/Event/Reducer)嵌入MVVM的ViewModel中,既保留原有架构的优势,又弥补其不足,大幅提升项目的可维护性和可扩展性。