MVI
MVI(Model-View-Intent)是一种基于单向数据流和状态驱动的架构模式,起源于前端(如 Cycle.js),后被引入 Android 开发。其核心是通过唯一的状态流和意图(Intent) 实现组件间的解耦,适合复杂交互场景。以下从核心概念、代码示例、应用场景及优缺点展开详解:
MVI 核心组件与单向数据流
MVI 的核心是 “单向数据流”,数据在组件间按固定方向流动,状态变化可预测,便于调试和测试。
| 组件 | 职责 | Android 对应实现 |
|---|---|---|
| Model(模型) | 不仅包含数据,还包含UI 状态(State)(如加载中、成功、失败、表单数据等),是视图的唯一数据来源。 | 数据类(如 LoginState)、仓库(Repository) |
| View(视图) | 被动展示 Model 中的 State,不包含业务逻辑;用户交互转化为 Intent 发送给 Presenter/ViewModel。 | Activity、Fragment、自定义 View |
| Intent(意图) | 封装用户交互行为(如点击登录、输入文本),是 View 向业务层传递指令的唯一方式。 | 密封类(Sealed Class)或枚举(如 LoginIntent) |
| ViewModel(处理器) | 接收 Intent,处理业务逻辑(调用 Model 层),生成新的 State 并发送给 View。 | 基于 LiveData 或 Flow 的 ViewModel |
单向数据流图示
用户交互 → View → Intent → ViewModel → 处理逻辑(调用Model)→ 新State → View(更新UI)
↑ ↓
└──────────────────────────────────┘
(State闭环:View始终基于最新State渲染)
Java 代码示例(登录功能)
以登录功能为例,展示 MVI 各组件的协作(注:Java 中无密封类,用接口 + 实现类模拟 Intent):
1. Intent(用户意图封装)
定义用户可能的交互行为(点击登录、输入文本等):
// 意图基类
public interface LoginIntent {
// 输入用户名
class UsernameChanged implements LoginIntent {
private final String username;
public UsernameChanged(String username) { this.username = username; }
public String getUsername() { return username; }
}
// 输入密码
class PasswordChanged implements LoginIntent {
private final String password;
public PasswordChanged(String password) { this.password = password; }
public String getPassword() { return password; }
}
// 点击登录按钮
class LoginClicked implements LoginIntent {}
}
2. State(UI 状态模型)
封装 View 所需的所有状态(加载中、错误信息、表单数据等),是不可变对象(每次更新生成新实例):
public class LoginState {
// 表单数据
private final String username;
private final String password;
// 状态标识
private final boolean isLoading;
private final String errorMessage;
private final boolean isLoginSuccess;
// 构造函数(私有,通过Builder创建)
private LoginState(String username, String password, boolean isLoading,
String errorMessage, boolean isLoginSuccess) {
this.username = username;
this.password = password;
this.isLoading = isLoading;
this.errorMessage = errorMessage;
this.isLoginSuccess = isLoginSuccess;
}
// 初始状态(静态工厂方法)
public static LoginState initial() {
return new LoginState("", "", false, null, false);
}
// Builder模式(便于生成新状态)
public static class Builder {
private String username;
private String password;
private boolean isLoading;
private String errorMessage;
private boolean isLoginSuccess;
public Builder from(LoginState state) { // 基于现有状态复制
this.username = state.username;
this.password = state.password;
this.isLoading = state.isLoading;
this.errorMessage = state.errorMessage;
this.isLoginSuccess = state.isLoginSuccess;
return this;
}
public Builder username(String username) {
this.username = username;
return this;
}
public Builder password(String password) {
this.password = password;
return this;
}
public Builder isLoading(boolean isLoading) {
this.isLoading = isLoading;
return this;
}
public Builder errorMessage(String errorMessage) {
this.errorMessage = errorMessage;
return this;
}
public Builder isLoginSuccess(boolean isLoginSuccess) {
this.isLoginSuccess = isLoginSuccess;
return this;
}
public LoginState build() {
return new LoginState(username, password, isLoading, errorMessage, isLoginSuccess);
}
}
// getter(无setter,确保不可变)
public String getUsername() { return username; }
public String getPassword() { return password; }
public boolean isLoading() { return isLoading; }
public String getErrorMessage() { return errorMessage; }
public boolean isLoginSuccess() { return isLoginSuccess; }
}
3. Model 层(数据与业务逻辑)
包含数据仓库和业务处理(登录验证):
// 数据模型
public class User {
private String username;
private String password;
// 构造函数、getter
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() { return username; }
public String getPassword() { return password; }
}
// 登录仓库(处理数据请求)
public class LoginRepository {
// 模拟网络登录
public void login(User user, LoginCallback callback) {
new Thread(() -> {
try {
Thread.sleep(1000); // 模拟网络延迟
if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) {
callback.onSuccess();
} else {
callback.onError("用户名或密码错误");
}
} catch (InterruptedException e) {
callback.onError("网络异常");
}
}).start();
}
public interface LoginCallback {
void onSuccess();
void onError(String message);
}
}
4. ViewModel(处理 Intent 并生成 State)
接收 Intent,处理逻辑,通过 LiveData 发送新 State 给 View:
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class LoginViewModel extends ViewModel {
private final MutableLiveData<LoginState> stateLiveData = new MutableLiveData<>();
private final LoginRepository repository;
private LoginState currentState;
public LoginViewModel() {
this.repository = new LoginRepository();
this.currentState = LoginState.initial(); // 初始状态
stateLiveData.setValue(currentState);
}
// 暴露State流给View
public LiveData<LoginState> getState() {
return stateLiveData;
}
// 接收View发送的Intent
public void processIntent(LoginIntent intent) {
if (intent instanceof LoginIntent.UsernameChanged) {
handleUsernameChanged(((LoginIntent.UsernameChanged) intent).getUsername());
} else if (intent instanceof LoginIntent.PasswordChanged) {
handlePasswordChanged(((LoginIntent.PasswordChanged) intent).getPassword());
} else if (intent instanceof LoginIntent.LoginClicked) {
handleLoginClicked();
}
}
// 处理用户名输入
private void handleUsernameChanged(String username) {
currentState = new LoginState.Builder()
.from(currentState)
.username(username)
.errorMessage(null) // 输入时清空错误
.build();
stateLiveData.setValue(currentState);
}
// 处理密码输入
private void handlePasswordChanged(String password) {
currentState = new LoginState.Builder()
.from(currentState)
.password(password)
.errorMessage(null)
.build();
stateLiveData.setValue(currentState);
}
// 处理登录点击
private void handleLoginClicked() {
// 1. 显示加载中
currentState = new LoginState.Builder()
.from(currentState)
.isLoading(true)
.errorMessage(null)
.isLoginSuccess(false)
.build();
stateLiveData.setValue(currentState);
// 2. 调用仓库登录
User user = new User(currentState.getUsername(), currentState.getPassword());
repository.login(user, new LoginRepository.LoginCallback() {
@Override
public void onSuccess() {
// 登录成功:更新状态
currentState = new LoginState.Builder()
.from(currentState)
.isLoading(false)
.isLoginSuccess(true)
.build();
stateLiveData.postValue(currentState); // 子线程用postValue
}
@Override
public void onError(String message) {
// 登录失败:更新状态
currentState = new LoginState.Builder()
.from(currentState)
.isLoading(false)
.errorMessage(message)
.build();
stateLiveData.postValue(currentState);
}
});
}
}
5. View(Activity 渲染 UI 并发送 Intent)
观察 State 变化并渲染 UI,用户交互转化为 Intent 发送给 ViewModel:
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
public class LoginActivity extends AppCompatActivity {
private EditText etUsername;
private EditText etPassword;
private Button btnLogin;
private ProgressBar progressBar;
private TextView tvError;
private LoginViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
initView();
// 初始化ViewModel
viewModel = new ViewModelProvider(this).get(LoginViewModel.class);
// 观察State变化,更新UI
viewModel.getState().observe(this, new Observer<LoginState>() {
@Override
public void onChanged(LoginState state) {
updateUI(state);
}
});
// 绑定用户交互到Intent
etUsername.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
// 发送用户名变化Intent
viewModel.processIntent(new LoginIntent.UsernameChanged(s.toString()));
}
// 其他方法省略
});
etPassword.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
// 发送密码变化Intent
viewModel.processIntent(new LoginIntent.PasswordChanged(s.toString()));
}
// 其他方法省略
});
btnLogin.setOnClickListener(v -> {
// 发送登录点击Intent
viewModel.processIntent(new LoginIntent.LoginClicked());
});
}
// 根据State更新UI
private void updateUI(LoginState state) {
// 更新输入框(可选,因输入框已双向绑定)
etUsername.setText(state.getUsername());
etPassword.setText(state.getPassword());
// 显示/隐藏加载框
progressBar.setVisibility(state.isLoading() ? View.VISIBLE : View.GONE);
// 显示错误信息
tvError.setText(state.getErrorMessage() != null ? state.getErrorMessage() : "");
// 登录成功处理
if (state.isLoginSuccess()) {
Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
finish();
}
}
private void initView() {
etUsername = findViewById(R.id.et_username);
etPassword = findViewById(R.id.et_password);
btnLogin = findViewById(R.id.btn_login);
progressBar = findViewById(R.id.progress_bar);
tvError = findViewById(R.id.tv_error);
}
}
MVI 的应用场景
- 复杂交互场景:如表单提交(多输入验证)、购物车(增删改查 + 状态同步)等,状态变化频繁且需统一管理。
- 需要可预测性的应用:如金融类 App,状态变化需严格追踪,便于调试和问题定位。
- 团队协作项目:单向数据流和强类型约束(如 Intent、State)可规范代码风格,降低沟通成本。
优点
- 单向数据流:数据流向清晰(Intent→ViewModel→State→View),调试时可追踪完整状态变化链路。
- 状态驱动 UI:View 完全基于 State 渲染,避免手动更新 UI 导致的不一致问题(如 “忘记更新某个控件”)。
- 可测试性强:ViewModel 处理逻辑与 View 解耦,State 和 Intent 是纯数据类,可通过单元测试验证状态转换。
- 状态可复现:State 包含 UI 完整信息,便于实现 “状态保存与恢复”(如屏幕旋转、进程重建)。
缺点
- 样板代码多:需定义大量 Intent、State 类,简单场景下显得冗余(Java 中尤为明显,Kotlin 可通过数据类简化)。
- 内存开销:每次状态更新都会创建新的 State 对象(不可变特性),频繁更新可能增加内存消耗。
- 学习成本高:相比 MVC/MVP,需要理解 “单向数据流”“不可变状态” 等概念,初期上手较难。
- 不适合简单场景:如静态页面、简单列表展示,使用 MVI 会过度设计。
总结
MVI 是一种以状态为核心的架构模式,通过单向数据流实现了 UI 与业务逻辑的解耦,适合复杂交互场景。其优点是状态可预测、可测试性强,缺点是样板代码多、学习成本高。在 Android 开发中,推荐结合 Kotlin 协程和 Flow 使用(简化状态流处理),Java 中因语法限制会略显繁琐,但核心思想仍可借鉴
资料
MVI-hannesdorfmann
MVI-简书
MVI-简书2
Android最佳架构:MVI + LiveData + ViewModel | ProAndroidDev