MVI 模式及mvp,mvvm对比

一、MVI 模式实现(登录场景)

MVI(Model-View-Intent)核心是单向数据流:View 发送 Intent → Model 处理并生成新 State → View 接收 State 刷新 UI,所有状态变化可追溯、可预测。

核心概念
  • Intent:用户行为(如 "登录请求""清空输入"),是 View 向 Model 发送的唯一指令。
  • State:页面的完整状态(如加载中、登录成功 / 失败、输入框内容),是 View 渲染的唯一依据。
  • Model:处理 Intent,结合数据层逻辑生成新 State。
  • View:仅负责渲染 State、发送 Intent,无任何业务逻辑。
完整代码实现
1. 配置基础依赖(协程 + LiveData)

gradle

复制代码
dependencies {
    // 协程
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
    // ViewModel + LiveData
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
}
2. 定义 Intent(用户行为)

kotlin

复制代码
// LoginIntent.kt
sealed class LoginIntent {
    // 登录请求Intent(携带用户名/密码)
    data class LoginRequest(val username: String, val password: String) : LoginIntent()
    // 清空输入Intent
    object ClearInput : LoginIntent()
}
3. 定义 State(页面状态)

kotlin

复制代码
// LoginState.kt
sealed class LoginState {
    // 初始状态
    object Idle : LoginState()
    // 加载中状态
    object Loading : LoginState()
    // 登录成功状态
    data class LoginSuccess(val msg: String) : LoginState()
    // 登录失败状态
    data class LoginFailure(val errorMsg: String) : LoginState()
    // 输入内容变化状态
    data class InputChanged(val username: String, val password: String) : LoginState()
}
4. Model 层(数据处理,复用逻辑)

kotlin

复制代码
// LoginRepository.kt(替代原Model,更贴合MVI命名)
class LoginRepository {
    suspend fun login(username: String, password: String): String {
        // 模拟异步请求(协程IO线程)
        return withContext(Dispatchers.IO) {
            delay(1000)
            when {
                username.isBlank() -> "用户名不能为空"
                password.isBlank() -> "密码不能为空"
                username == "admin" && password == "123456" -> "登录成功!"
                else -> "用户名或密码错误"
            }
        }
    }
}
5. ViewModel 层(核心:处理 Intent,生成 State)

kotlin

复制代码
// LoginViewModel.kt
class LoginViewModel : ViewModel() {
    // 接收Intent的管道(Channel:协程安全的消息传递)
    private val intentChannel = Channel<LoginIntent>(Channel.UNLIMITED)
    // 页面状态(LiveData:通知View刷新)
    private val _state = MutableLiveData<LoginState>(LoginState.Idle)
    val state: LiveData<LoginState> = _state

    private val repository = LoginRepository()
    private var currentUsername = ""
    private var currentPassword = ""

    init {
        // 监听Intent并处理
        handleIntent()
    }

    // 接收View发送的Intent
    fun sendIntent(intent: LoginIntent) {
        viewModelScope.launch {
            intentChannel.send(intent)
        }
    }

    // 处理Intent的核心逻辑
    private fun handleIntent() {
        viewModelScope.launch {
            intentChannel.consumeAsFlow().collect { intent ->
                when (intent) {
                    is LoginIntent.LoginRequest -> handleLogin(intent.username, intent.password)
                    LoginIntent.ClearInput -> {
                        currentUsername = ""
                        currentPassword = ""
                        _state.postValue(LoginState.InputChanged("", ""))
                    }
                }
            }
        }
    }

    // 处理登录逻辑
    private suspend fun handleLogin(username: String, password: String) {
        // 更新当前输入状态
        currentUsername = username
        currentPassword = password
        _state.postValue(LoginState.Loading)

        // 调用Repository获取结果
        val result = repository.login(username, password)
        // 生成最终State
        val newState = if (result == "登录成功!") {
            LoginState.LoginSuccess(result)
        } else {
            LoginState.LoginFailure(result)
        }
        _state.postValue(newState)
    }
}
6. View 层(Activity + 布局)

kotlin

复制代码
// LoginActivity.kt
class LoginActivity : AppCompatActivity() {
    private lateinit var viewModel: LoginViewModel
    private lateinit var etUsername: EditText
    private lateinit var etPassword: EditText
    private lateinit var btnLogin: Button
    private lateinit var progressDialog: ProgressDialog

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // 初始化控件
        etUsername = findViewById(R.id.et_username)
        etPassword = findViewById(R.id.et_password)
        btnLogin = findViewById(R.id.btn_login)
        progressDialog = ProgressDialog(this).apply {
            setMessage("登录中...")
            setCancelable(false)
        }

        // 初始化ViewModel
        viewModel = ViewModelProvider(this)[LoginViewModel::class.java]

        // 监听State变化,刷新UI
        observeState()

        // 登录按钮点击:发送LoginRequest Intent
        btnLogin.setOnClickListener {
            val username = etUsername.text.toString().trim()
            val password = etPassword.text.toString().trim()
            viewModel.sendIntent(LoginIntent.LoginRequest(username, password))
        }

        // 输入框变化:模拟发送InputChanged(简化版,实际可封装)
        etUsername.addTextChangedListener {
            viewModel.sendIntent(LoginIntent.ClearInput) // 示例:仅演示Intent发送
        }
    }

    // 核心:观察State并更新UI
    private fun observeState() {
        viewModel.state.observe(this) { state ->
            when (state) {
                LoginState.Idle -> {}
                LoginState.Loading -> progressDialog.show()
                is LoginState.LoginSuccess -> {
                    progressDialog.dismiss()
                    Toast.makeText(this, state.msg, Toast.LENGTH_SHORT).show()
                    startActivity(Intent(this, MainActivity::class.java))
                    finish()
                }
                is LoginState.LoginFailure -> {
                    progressDialog.dismiss()
                    Toast.makeText(this, state.errorMsg, Toast.LENGTH_SHORT).show()
                }
                is LoginState.InputChanged -> {
                    etUsername.setText(state.username)
                    etPassword.setText(state.password)
                }
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        progressDialog.dismiss()
    }
}
7. 布局文件(activity_login.xml)

xml

复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="20dp">

    <EditText
        android:id="@+id/et_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名"
        android:inputType="text"
        android:maxLines="1"/>

    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:hint="请输入密码"
        android:inputType="textPassword"
        android:maxLines="1"/>

    <Button
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="登录"/>

</LinearLayout>

二、MVP/MVVM/MVI 核心对比

维度 MVP(Model-View-Presenter) MVVM(Model-View-ViewModel) MVI(Model-View-Intent)
核心思想 中间层解耦:Presenter 协调 View 和 Model 数据驱动:ViewModel 持有 LiveData,DataBinding 绑定 UI 单向数据流:Intent→Model→State→View,状态唯一且可预测
数据流 双向(View→Presenter→Model,Model→Presenter→View) 双向(DataBinding 双向绑定)+ 单向(LiveData 推送) 严格单向(Intent→State→View)
层职责 - View:UI 展示 + 交互- Presenter:业务逻辑 + 线程切换- Model:数据处理 - View:UI 展示 + 观察 LiveData- ViewModel:业务逻辑 + 持有状态- Model:数据处理 - View:发送 Intent + 渲染 State- Model:处理 Intent 生成 State- Intent:用户行为封装- State:页面完整状态
状态管理 分散(Presenter 临时持有,易不一致) 集中(ViewModel 持有 LiveData,但可多数据源修改) 唯一可信源(State 是页面唯一状态,不可变)
生命周期 Presenter 需手动管理(弱引用 + detach),易内存泄漏 ViewModel 由系统管理,生命周期独立于 Activity ViewModel 管理,State 全量存储,旋转不丢失
可测试性 中等(Presenter 可 Mock View,但需处理线程) 高(ViewModel 无 View 引用,可独立测试) 极高(State/Intent 纯数据类,逻辑无副作用)
复杂度 低 - 中等(易上手,适合小项目) 中等(DataBinding/LiveData 有学习成本) 高(需封装 Intent/State,适合复杂页面)
适用场景 小型项目、快速迭代、团队技术栈简单 中大型项目、数据驱动 UI、Jetpack 生态 复杂页面(如表单 / 电商)、状态需追溯 / 回滚、团队规范高
核心问题 Presenter 易膨胀,状态分散导致 UI 不一致 双向绑定调试困难,状态修改入口多 模板代码多,简单页面 "过度设计"

三、选型建议

  1. MVP:适合新手入门、小型项目或维护老项目,核心优势是 "简单易理解",但需注意 Presenter 内存泄漏问题。
  2. MVVM:Android 官方推荐,中大型项目首选,结合 Jetpack(ViewModel/LiveData/Compose)可大幅提升开发效率,缺点是双向绑定调试稍复杂。
  3. MVI:适合状态复杂的场景(如金融 / 电商表单、多状态切换页面),核心优势是 "状态可预测、可追溯",但需额外封装 Intent/State,小项目不建议使用。

总结:MVI 是 MVVM 的 "强化版",通过单向数据流解决了 MVVM 状态分散的问题,但学习和开发成本更高;MVP 是最基础的解耦模式,适合快速落地。实际项目中可混合使用(如简单页面用 MVP/MVVM,复杂页面用 MVI)。

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui