Android Kotlin MVVM与MVI组合场景

9 阅读10分钟

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中,既保留原有架构的优势,又弥补其不足,大幅提升项目的可维护性和可扩展性。