一、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 不一致 | 双向绑定调试困难,状态修改入口多 | 模板代码多,简单页面 "过度设计" |
三、选型建议
- MVP:适合新手入门、小型项目或维护老项目,核心优势是 "简单易理解",但需注意 Presenter 内存泄漏问题。
- MVVM:Android 官方推荐,中大型项目首选,结合 Jetpack(ViewModel/LiveData/Compose)可大幅提升开发效率,缺点是双向绑定调试稍复杂。
- MVI:适合状态复杂的场景(如金融 / 电商表单、多状态切换页面),核心优势是 "状态可预测、可追溯",但需额外封装 Intent/State,小项目不建议使用。
总结:MVI 是 MVVM 的 "强化版",通过单向数据流解决了 MVVM 状态分散的问题,但学习和开发成本更高;MVP 是最基础的解耦模式,适合快速落地。实际项目中可混合使用(如简单页面用 MVP/MVVM,复杂页面用 MVI)。