一、MVI 是什么
MVI(Model-View-Intent)是一种基于单向数据流的架构模式,核心思想是:
- Intent:用户意图/事件(如点击、输入、刷新),是改变状态的唯一来源
- ViewModel:接收 Intent,处理业务逻辑,生成新的 State
- State:不可变的单一状态对象,包含页面所有 UI 信息
- View:观察 State,纯函数式渲染 UI,不持有业务逻辑
核心公式:View = f(State) ------ UI 只是状态的纯函数映射。
二、完整 Demo:用户状态管理页面
实现一个用户信息页面,演示 MVI 的完整数据流。
1. 依赖配置(build.gradle)
groovy
apply plugin: 'com.android.application'
android {
compileSdkVersion 33
defaultConfig {
applicationId "com.example.mvidemo"
minSdkVersion 21
targetSdkVersion 33
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
// ViewModel + LiveData
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.2.0'
}
2. Model 层
User.java
java
package com.example.mvidemo.model;
public class User {
private String name;
private int age;
private String bio;
public User(String name, int age, String bio) {
this.name = name;
this.age = age;
this.bio = bio;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getBio() { return bio; }
public void setBio(String bio) { this.bio = bio; }
}
3. State 层(MVI 核心)
UserState.java ------ 单一不可变状态
java
package com.example.mvidemo.state;
import com.example.mvidemo.model.User;
/**
* State:页面的唯一状态源
* 特点:
* 1. 不可变(Immutable):状态变更时必须创建新对象
* 2. 包含页面所有 UI 信息:数据、加载态、错误信息
* 3. View 只需观察这一个对象即可渲染完整界面
*/
public class UserState {
// 用户数据
private final User user;
// 是否加载中
private final boolean loading;
// 错误信息(null 表示无错误)
private final String error;
// 操作成功提示(如保存成功)
private final String successMessage;
// 私有构造器,强制通过 Builder 创建
private UserState(Builder builder) {
this.user = builder.user;
this.loading = builder.loading;
this.error = builder.error;
this.successMessage = builder.successMessage;
}
// ========== Getter(只有 Getter,没有 Setter,保证不可变)==========
public User getUser() { return user; }
public boolean isLoading() { return loading; }
public String getError() { return error; }
public String getSuccessMessage() { return successMessage; }
// ========== Builder 模式:基于当前状态创建新状态 ==========
public Builder copy() {
return new Builder(this);
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private User user;
private boolean loading;
private String error;
private String successMessage;
public Builder() {}
public Builder(UserState state) {
this.user = state.user;
this.loading = state.loading;
this.error = state.error;
this.successMessage = state.successMessage;
}
public Builder user(User user) {
this.user = user;
return this;
}
public Builder loading(boolean loading) {
this.loading = loading;
return this;
}
public Builder error(String error) {
this.error = error;
return this;
}
public Builder successMessage(String msg) {
this.successMessage = msg;
return this;
}
public UserState build() {
return new UserState(this);
}
}
// 空状态(初始状态)
public static UserState idle() {
return builder().build();
}
}
4. Intent 层(MVI 核心)
UserIntent.java ------ 用户意图密封类
java
package com.example.mvidemo.intent;
import com.example.mvidemo.model.User;
/**
* Intent:用户意图/事件
* 在 Java 中通过抽象类 + 静态内部类模拟 Kotlin 的密封类(Sealed Class)
*
* 所有能改变状态的操作,必须定义为一个 Intent:
* - 用户点击按钮 → Intent
* - 页面初始化 → Intent
* - 输入框变化 → Intent
*
* ViewModel 只接受 Intent,不接受直接的方法调用
*/
public abstract class UserIntent {
/** 加载用户 */
public static class LoadUser extends UserIntent {
private final String userId;
public LoadUser(String userId) {
this.userId = userId;
}
public String getUserId() {
return userId;
}
}
/** 更新用户名 */
public static class UpdateName extends UserIntent {
private final String newName;
public UpdateName(String newName) {
this.newName = newName;
}
public String getNewName() {
return newName;
}
}
/** 保存用户 */
public static class SaveUser extends UserIntent {}
/** 刷新页面 */
public static class Refresh extends UserIntent {}
/** 清除错误(用户已消费错误提示) */
public static class ClearError extends UserIntent {}
}
5. Repository 层
UserRepository.java
java
package com.example.mvidemo.repository;
import android.os.Handler;
import android.os.Looper;
import com.example.mvidemo.model.User;
/**
* Repository:数据仓库
* 职责:屏蔽数据来源,为 ViewModel 提供统一数据接口
*/
public class UserRepository {
public void getUserFromServer(String userId, Callback callback) {
new Thread(() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟 20% 概率失败
if (Math.random() > 0.8) {
new Handler(Looper.getMainLooper()).post(() -> {
callback.onError("网络连接失败,请重试");
});
return;
}
User user = new User("张三", 25, "Android 开发工程师,热爱技术");
new Handler(Looper.getMainLooper()).post(() -> {
callback.onSuccess(user);
});
}).start();
}
public void saveUserToServer(User user, Callback callback) {
new Thread(() -> {
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Handler(Looper.getMainLooper()).post(() -> {
callback.onSuccess(null);
});
}).start();
}
public interface Callback {
void onSuccess(User user);
void onError(String error);
}
}
6. ViewModel 层(MVI 核心)
UserViewModel.java
java
package com.example.mvidemo.viewmodel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.mvidemo.intent.UserIntent;
import com.example.mvidemo.model.User;
import com.example.mvidemo.repository.UserRepository;
import com.example.mvidemo.state.UserState;
/**
* ViewModel:状态处理器
*
* 核心职责:
* 1. 持有唯一的 State(通过 LiveData 暴露)
* 2. 接收 Intent,处理业务逻辑,生成新的不可变 State
* 3. 不持有 View 引用,不直接操作 UI
*
* MVI 中的 ViewModel 是一个"状态机":
* Intent → 处理逻辑 → 新 State → View 自动刷新
*/
public class UserViewModel extends ViewModel {
// 单一状态源:页面所有 UI 信息都在这个对象里
private final MutableLiveData<UserState> stateLiveData = new MutableLiveData<>(UserState.idle());
private final UserRepository repository = new UserRepository();
/** 对外暴露不可变的 State */
public LiveData<UserState> getState() {
return stateLiveData;
}
/**
* 唯一入口:处理用户意图
* 所有能改变状态的操作,都必须通过这个方法
*/
public void processIntent(UserIntent intent) {
if (intent instanceof UserIntent.LoadUser) {
handleLoadUser((UserIntent.LoadUser) intent);
} else if (intent instanceof UserIntent.UpdateName) {
handleUpdateName((UserIntent.UpdateName) intent);
} else if (intent instanceof UserIntent.SaveUser) {
handleSaveUser();
} else if (intent instanceof UserIntent.Refresh) {
handleRefresh();
} else if (intent instanceof UserIntent.ClearError) {
handleClearError();
}
}
// ========== 私有方法:处理具体 Intent,生成新 State ==========
private void handleLoadUser(UserIntent.LoadUser intent) {
// 1. 基于当前状态,创建"加载中"的新状态
UserState current = stateLiveData.getValue();
UserState loadingState = current.copy()
.loading(true)
.error(null)
.build();
stateLiveData.setValue(loadingState);
// 2. 请求数据
repository.getUserFromServer(intent.getUserId(), new UserRepository.Callback() {
@Override
public void onSuccess(User user) {
// 3. 成功:创建"加载完成+有数据"的新状态
UserState successState = stateLiveData.getValue().copy()
.user(user)
.loading(false)
.build();
stateLiveData.setValue(successState);
}
@Override
public void onError(String error) {
// 4. 失败:创建"加载完成+有错误"的新状态
UserState errorState = stateLiveData.getValue().copy()
.loading(false)
.error(error)
.build();
stateLiveData.setValue(errorState);
}
});
}
private void handleUpdateName(UserIntent.UpdateName intent) {
UserState current = stateLiveData.getValue();
User user = current.getUser();
if (user != null) {
user.setName(intent.getNewName());
// 创建新状态对象(不可变原则)
UserState newState = current.copy()
.user(user)
.build();
stateLiveData.setValue(newState);
}
}
private void handleSaveUser() {
UserState current = stateLiveData.getValue();
User user = current.getUser();
if (user == null) return;
stateLiveData.setValue(current.copy().loading(true).build());
repository.saveUserToServer(user, new UserRepository.Callback() {
@Override
public void onSuccess(User user) {
UserState newState = stateLiveData.getValue().copy()
.loading(false)
.successMessage("保存成功!")
.build();
stateLiveData.setValue(newState);
}
@Override
public void onError(String error) {
UserState newState = stateLiveData.getValue().copy()
.loading(false)
.error(error)
.build();
stateLiveData.setValue(newState);
}
});
}
private void handleRefresh() {
UserState current = stateLiveData.getValue();
User user = current.getUser();
if (user != null) {
processIntent(new UserIntent.LoadUser("refresh"));
}
}
private void handleClearError() {
UserState current = stateLiveData.getValue();
stateLiveData.setValue(current.copy().error(null).successMessage(null).build());
}
}
7. View 层
activity_main.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:orientation="vertical"
android:padding="20dp">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="8dp" />
<TextView
android:id="@+id/tvBio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_marginTop="8dp" />
<Button
android:id="@+id/btnLoad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="加载用户" />
<Button
android:id="@+id/btnUpdate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="修改名字为 李四" />
<Button
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="保存到服务器" />
</LinearLayout>
MainActivity.java
java
package com.example.mvidemo;
import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import com.example.mvidemo.intent.UserIntent;
import com.example.mvidemo.state.UserState;
import com.example.mvidemo.viewmodel.UserViewModel;
/**
* View 层:Activity
*
* 核心职责:
* 1. 初始化 ViewModel
* 2. 观察单一 State,根据 State 渲染完整 UI
* 3. 将用户操作转换为 Intent,发送给 ViewModel
*
* 设计原则:
* - 不持有业务逻辑
* - 不直接修改数据
* - UI 只是 State 的纯函数映射:render(State) → UI
*/
public class MainActivity extends AppCompatActivity {
private UserViewModel viewModel;
private ProgressBar progressBar;
private TextView tvName, tvAge, tvBio;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化 View
progressBar = findViewById(R.id.progressBar);
tvName = findViewById(R.id.tvName);
tvAge = findViewById(R.id.tvAge);
tvBio = findViewById(R.id.tvBio);
// 获取 ViewModel
viewModel = new ViewModelProvider(this).get(UserViewModel.class);
// ========== 核心:观察单一 State,驱动 UI 渲染 ==========
viewModel.getState().observe(this, this::render);
// ========== 用户操作 → 转换为 Intent → 发送给 ViewModel ==========
findViewById(R.id.btnLoad).setOnClickListener(v -> {
viewModel.processIntent(new UserIntent.LoadUser("user_001"));
});
findViewById(R.id.btnUpdate).setOnClickListener(v -> {
viewModel.processIntent(new UserIntent.UpdateName("李四"));
});
findViewById(R.id.btnSave).setOnClickListener(v -> {
viewModel.processIntent(new UserIntent.SaveUser());
});
}
/**
* 渲染函数:根据 State 刷新完整 UI
* 这是 MVI 的核心:View = f(State)
*/
private void render(UserState state) {
// 1. 渲染加载状态
progressBar.setVisibility(state.isLoading() ? View.VISIBLE : View.GONE);
// 2. 渲染用户数据
if (state.getUser() != null) {
tvName.setText(state.getUser().getName());
tvAge.setText(state.getUser().getAge() + " 岁");
tvBio.setText(state.getUser().getBio());
}
// 3. 渲染错误信息
if (state.getError() != null) {
Toast.makeText(this, "错误:" + state.getError(), Toast.LENGTH_SHORT).show();
// 消费后清除,避免重复弹出
viewModel.processIntent(new UserIntent.ClearError());
}
// 4. 渲染成功提示
if (state.getSuccessMessage() != null) {
Toast.makeText(this, state.getSuccessMessage(), Toast.LENGTH_SHORT).show();
viewModel.processIntent(new UserIntent.ClearError());
}
}
}
三、每个模块作用详解
| 模块 | 职责 | 为什么这样设计 |
|---|---|---|
Model (User) |
定义数据结构 | 与 MVVM 相同,纯 POJO |
State (UserState) |
包含页面所有 UI 状态的不可变对象 | 单一状态源,任何时刻 UI 都能从 State 完整还原,避免状态分散导致的不一致 |
Intent (UserIntent) |
定义所有能改变状态的用户操作 | 所有状态变更必须经过 Intent,形成可追溯的"事件日志",便于调试和埋点 |
Repository (UserRepository) |
数据获取统一入口 | 与 MVVM 相同,屏蔽数据来源 |
| ViewModel | 状态机:接收 Intent → 处理逻辑 → 输出新 State | 不暴露多个 LiveData,只暴露一个 State,强制所有状态变更走统一流程 |
View (Activity) |
纯渲染函数 render(State),发送 Intent |
不包含任何 if/else 业务判断,只负责把 State 映射到 UI 控件 |
四、MVI 的数据流向图
用户点击按钮
│
▼
┌─────────────┐
│ Intent │ ← 用户意图(LoadUser/UpdateName/SaveUser)
└──────┬──────┘
│
▼
┌─────────────┐
│ ViewModel │
│ processIntent│
└──────┬──────┘
│
▼
┌─────────────┐
│ 业务逻辑处理 │ ← 调用 Repository、计算、转换
└──────┬──────┘
│
▼
┌─────────────┐
│ New State │ ← 全新的不可变状态对象
│(copy + 修改)│
└──────┬──────┘
│
▼
┌─────────────┐
│ render() │ ← Activity 中的纯函数
└──────┬──────┘
│
▼
UI 刷新
关键特征:
- 单向:数据永远从 Intent → ViewModel → State → View,不会反向流动
- 不可变:State 一旦创建不可修改,变更必须创建新对象
- 纯函数:render(State) 只依赖输入的状态,不依赖外部变量
五、MVI 的优势与缺点
✅ 优势
1. 单一状态源(Single Source of Truth)
页面所有状态收敛到一个 State 对象,不会出现"数据不一致"的 Bug
2. 状态可追溯、可预测
每个 UI 状态都是由 Intent 经过纯函数计算得出,给定相同的 Intent 序列,一定得到相同的 State 序列
3. 单向数据流,易于调试
- 数据流向单一,不会形成 MVVM 中多个 LiveData 交叉观察的"回调地狱"
- 可以记录所有 Intent 序列,实现"时间旅行调试"(Time Travel Debugging)
4. 渲染逻辑集中
所有 UI 更新集中在 render() 一个函数中,代码结构清晰
5. 天然支持响应式编程
与 RxJava、Kotlin Flow、Compose 等响应式框架思想高度契合
❌ 缺点
1. 样板代码多
- 每个页面都需要定义 State、Intent、render(),简单页面显得过重
- Java 中缺乏密封类和 Data Class,实现不可变 State 需要写大量 Builder 代码
2. State 膨胀问题
- 随着页面复杂度增加,State 类会变得非常大,包含大量字段
- 一次小的状态变更也需要创建完整的 State 新对象,存在性能开销
3. 一次性事件处理麻烦
Toast、Snackbar、页面跳转等"一次性事件"不适合放在 State 中(因为配置变更后会重发)
需要引入 SingleLiveEvent 或 Channel 等额外机制
4. 学习曲线陡峭
需要理解函数式编程、不可变数据、单向数据流等概念
5. 不适合超复杂页面
如果页面有多个独立模块(如头部、列表、底部各不相关),单一 State 会导致不必要的全量刷新
六、MVVM vs MVI 对比
| 维度 | MVVM | MVI |
|---|---|---|
| 数据流 | 双向(View ↔ ViewModel) | 单向(Intent → State → View) |
| 状态管理 | 多个 LiveData,分散管理 | 单一 State 对象,集中管理 |
| 可变性 | LiveData 的值可变 | State 不可变,必须创建新对象 |
| 调试难度 | 多个数据流交叉,较难追踪 | 单一数据流,Intent 序列可追溯 |
| 代码量 | 较少 | 较多(State + Intent + render) |
| 适用场景 | 大多数业务页面 | 状态复杂、需要严格状态管理的页面 |
| 与 Compose 配合 | 可用 | 更契合(State 驱动重组) |
七、常见问题
1. MVI 和 MVVM 的本质区别是什么?
MVVM 是"多数据流":多个 LiveData 各自独立,View 观察多个数据源。MVI 是"单一数据流":所有状态收敛为一个不可变的 State,通过 Intent 驱动变更,UI 只是 State 的纯函数映射。
2. 什么是单向数据流?为什么 MVI 要强调这个?
数据永远从 Intent → ViewModel → State → View 单向流动。这样可以保证状态变更可追溯、可预测,避免 MVVM 中多个 LiveData 互相观察导致的"回调地狱"和状态不一致。
3. MVI 中的 State 为什么要设计为不可变?
不可变对象保证线程安全,避免并发修改问题;同时保证状态变更的"原子性"------任何时候 State 都是完整的、一致的,不会出现"改了一半"的中间状态被 UI 渲染。
4. MVI 如何处理一次性事件(如 Toast、页面跳转)?
一次性事件不适合放在 State 中,因为 State 是持久化的,配置变更(如旋转屏幕)后会重发事件。解决方案:
- 使用 SingleLiveEvent 单独处理事件流
- 使用 Kotlin 的 Channel / SharedFlow
- 在 State 中增加 consumable 标记位,消费后自动清除
5. State 膨胀了怎么办?
对于超复杂页面,可以将 State 拆分为多个子 State(如 HeaderState、ListState),但每个模块内部仍保持单一 State 原则。或者退化为 MVVM,对独立模块使用独立的 ViewModel。
6. MVI 中 ViewModel 的 processIntent() 方法有什么作用?
它是 ViewModel 的唯一入口,所有能改变状态的操作都必须封装为 Intent 通过此方法传入。这样 ViewModel 就是一个纯"状态机":接收输入(Intent)→ 处理逻辑 → 输出状态(State)。
7. Java 中如何实现 Kotlin 的密封类(Sealed Class)?
将 Intent 定义为 abstract class,所有具体意图定义为 public static class 内部类。外部只能通过这些内部类实例化,达到类似密封类的效果。
8. MVI 适合所有项目吗?
不适合。对于简单页面(如只有一两个按钮和文本),MVI 的样板代码(State + Intent + Builder)会显得过重。推荐在状态复杂、多人协作、需要严格状态管理的页面使用 MVI。
9. MVI 和 Redux 有什么关系?
MVI 的思想来源于 Redux(Flux 架构)。Redux 的核心也是:Action → Reducer → Store → View。MVI 的 Intent 对应 Action,ViewModel 对应 Store + Reducer,State 对应 Store 中的 State。
10. 如何在 MVI 中处理副作用(Side Effect)?
副作用指异步操作(网络请求、数据库读写)。在 MVI 中,副作用通常在 ViewModel 的 processIntent() 中触发,通过回调或协程获取结果后,再生成新的 State。更严格的实现会引入 Effect 概念,将副作用与状态变更分离。
八、总结
- MVVM 是"多数据流观察":View 观察多个 LiveData,各自独立更新。
- MVI 是"单一状态机":所有状态收敛为一个不可变的 State,通过 Intent 驱动变更,UI 只是 render(State) 的纯函数映射。
- 普通业务页面 → MVVM(代码量少,足够用)
- 状态复杂、需要严格状态管理、配合 Compose → MVI(可追溯、可预测)