架构模式:MVC / MVP / MVVM 对比
对比 Android 开发中三种主流架构模式 MVC、MVP、MVVM 的职责划分与代码组织方式,并以登录页为示例进行重构演示。
学习目标
- 理解 MVC、MVP、MVVM 三种架构模式的核心思想与职责划分
- 掌握 Contract 接口在 MVP 中的作用及编写方式
- 能够使用 ViewModel + LiveData 实现 MVVM 数据驱动 UI
- 根据项目场景选择合适的架构模式
学习目标
- 理解 MVC、MVP、MVVM 三种架构模式的核心思想与职责划分;
- 掌握 Contract 接口在 MVP 中的作用及编写方式;
- 能够使用 ViewModel + LiveData 实现 MVVM 数据驱动 UI;
- 根据项目场景选择合适的架构模式并知道各自取舍。
架构模式:MVC / MVP / MVVM 对比
为什么需要架构模式
Android 早期开发中,最容易出现的反模式是 “Massive View Controller”——Activity 或 Fragment 同时承担视图渲染、用户交互处理、网络请求、数据库访问、状态管理等所有职责。一个登录页面的 Activity 可能轻松超过 800 行,导致:
- 难以测试:UI 框架依赖耦合在业务逻辑里,无法用 JUnit 单测;
- 难以维护:业务逻辑和 UI 渲染代码交织,修改一处影响多处;
- 难以协作:UI 改动与逻辑改动冲突频繁;
- 难以复用:相同业务逻辑在多个页面重复实现。
架构模式的核心目标是 关注点分离(Separation of Concerns),让数据、逻辑、视图各司其职,并通过明确的接口约定彼此通信。
MVC(Model-View-Controller)
Android 中的 MVC 演变
经典 MVC 起源于桌面端:View 负责渲染,Controller 接收用户输入并更新 Model,Model 变化后再通知 View 刷新。在 Android 中,由于 Activity / Fragment 既是 View(持有布局)又是 Controller(处理事件),MVC 实际上演变成:
- Model:数据与业务逻辑,如
User、UserManager、LoginApi; - View:XML 布局 + Activity / Fragment 中渲染相关的代码;
- Controller:Activity / Fragment 中处理用户输入与业务调度的代码。
也就是说,Activity 既当 View 又当 Controller,导致 Activity 承担过多职责。下面是一个典型 MVC 风格的登录实现:
// Model:用户与登录接口
public class User {
public final String username;
public final String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
public class LoginManager {
public boolean login(String username, String password) {
// 实际项目应调用 Retrofit / OkHttp 异步请求
return "admin".equals(username) && "123456".equals(password);
}
}
// Activity 充当 View + Controller
public class MvcLoginActivity extends AppCompatActivity {
private EditText usernameEt;
private EditText passwordEt;
private Button loginBtn;
private TextView resultTv;
private LoginManager loginManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
usernameEt = findViewById(R.id.et_username);
passwordEt = findViewById(R.id.et_password);
loginBtn = findViewById(R.id.btn_login);
resultTv = findViewById(R.id.tv_result);
loginManager = new LoginManager();
loginBtn.setOnClickListener(v -> doLogin());
}
// Controller 职责:处理事件并调用 Model
private void doLogin() {
String username = usernameEt.getText().toString();
String password = passwordEt.getText().toString();
boolean ok = loginManager.login(username, password);
// View 职责:刷新 UI
if (ok) {
resultTv.setText("登录成功");
} else {
resultTv.setText("登录失败");
}
}
}
MVC 的优缺点
- 优点:上手简单,小型页面开发速度快;
- 缺点:Activity 职责过重、几乎无法单元测试、容易演变成“上帝对象”。
MVP(Model-View-Presenter)
职责划分
MVP 在 Android 中通过引入 Presenter 把业务逻辑从 Activity 剥离,Activity 只剩下 View 职责。三者职责为:
- Model:数据与业务逻辑(同 MVC);
- View:Activity / Fragment,只负责渲染 UI、转发用户事件;
- Presenter:处理业务逻辑,持有 View 接口引用,调用 Model 后通知 View 刷新。
View 与 Presenter 之间通过 Contract 接口 解耦,方便对 Presenter 进行单元测试(用 Mock View 替换真实 View)。
Contract 接口
Contract 把 View 接口与 Presenter 接口聚合在一处,便于统一查阅:
public interface LoginContract {
interface View {
void showLoading();
void hideLoading();
void showSuccess(String token);
void showError(String message);
}
interface Presenter {
void login(String username, String password);
void attachView(View view);
void detachView();
}
}
Presenter 实现
public class LoginPresenter implements LoginContract.Presenter {
private LoginContract.View view;
private final LoginManager loginManager;
public LoginPresenter(LoginManager loginManager) {
this.loginManager = loginManager;
}
@Override
public void attachView(LoginContract.View view) {
this.view = view;
}
@Override
public void detachView() {
this.view = null;
}
@Override
public void login(String username, String password) {
if (view == null) return;
view.showLoading();
boolean ok = loginManager.login(username, password);
view.hideLoading();
if (ok) {
view.showSuccess("token-" + System.currentTimeMillis());
} else {
view.showError("用户名或密码错误");
}
}
}
View 实现
public class MvpLoginActivity extends AppCompatActivity implements LoginContract.View {
private LoginPresenter presenter;
private ProgressBar loadingPb;
private TextView resultTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
loadingPb = findViewById(R.id.pb_loading);
resultTv = findViewById(R.id.tv_result);
presenter = new LoginPresenter(new LoginManager());
presenter.attachView(this);
findViewById(R.id.btn_login).setOnClickListener(v -> {
String u = ((EditText) findViewById(R.id.et_username)).getText().toString();
String p = ((EditText) findViewById(R.id.et_password)).getText().toString();
presenter.login(u, p);
});
}
@Override
public void showLoading() {
loadingPb.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading() {
loadingPb.setVisibility(View.GONE);
}
@Override
public void showSuccess(String token) {
resultTv.setText("登录成功:" + token);
}
@Override
public void showError(String message) {
resultTv.setText("失败:" + message);
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.detachView(); // 防止内存泄漏
}
}
MVP 的优缺点
- 优点:View 与业务逻辑解耦,Presenter 可被单元测试;
- 缺点:接口爆炸(每个页面都需要 Contract)、Presenter 与 View 生命周期管理繁琐(如配置变更时需手动保留 Presenter)、手写回调样板代码多。
MVVM(Model-View-ViewModel)
核心思想
MVVM 在 MVP 基础上把 “Presenter 主动调用 View 方法” 改为 “ViewModel 暴露可观察数据,View 自动响应”。Android 推荐使用 Jetpack 的 ViewModel + LiveData 实现:
- Model:数据层(Repository、网络、数据库);
- View:Activity / Fragment,订阅 ViewModel 的 LiveData;
- ViewModel:持有状态,处理业务逻辑,通过 LiveData 暴露状态,不持有 View 引用。
由于 ViewModel 不持有 View 引用,天然规避了内存泄漏;又因 LiveData 是生命周期感知的,配置变更时数据自动恢复。
ViewModel + LiveData 登录实现
public class LoginViewModel extends ViewModel {
public enum UiState { IDLE, LOADING, SUCCESS, ERROR }
private final MutableLiveData<UiState> state = new MutableLiveData<>(UiState.IDLE);
private final MutableLiveData<String> message = new MutableLiveData<>();
private final LoginManager loginManager = new LoginManager();
public LiveData<UiState> getState() {
return state;
}
public LiveData<String> getMessage() {
return message;
}
public void login(String username, String password) {
state.setValue(UiState.LOADING);
// 实际项目应使用 RxJava / 协程做异步,这里简化为同步
boolean ok = loginManager.login(username, password);
if (ok) {
state.setValue(UiState.SUCCESS);
message.setValue("token-" + System.currentTimeMillis());
} else {
state.setValue(UiState.ERROR);
message.setValue("用户名或密码错误");
}
}
}
public class MvvmLoginActivity extends AppCompatActivity {
private LoginViewModel viewModel;
private ProgressBar loadingPb;
private TextView resultTv;
private EditText usernameEt;
private EditText passwordEt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
loadingPb = findViewById(R.id.pb_loading);
resultTv = findViewById(R.id.tv_result);
usernameEt = findViewById(R.id.et_username);
passwordEt = findViewById(R.id.et_password);
viewModel = new ViewModelProvider(this).get(LoginViewModel.class);
viewModel.getState().observe(this, state -> {
switch (state) {
case LOADING:
loadingPb.setVisibility(View.VISIBLE);
break;
case SUCCESS:
loadingPb.setVisibility(View.GONE);
resultTv.setText("登录成功:" + viewModel.getMessage().getValue());
break;
case ERROR:
loadingPb.setVisibility(View.GONE);
resultTv.setText("失败");
break;
default:
break;
}
});
findViewById(R.id.btn_login).setOnClickListener(v -> {
viewModel.login(usernameEt.getText().toString(),
passwordEt.getText().toString());
});
}
}
MVVM 的优缺点
- 优点:无 View 引用、配置变更自动恢复状态、可观察数据驱动 UI;
- 缺点:数据流可能散落多处、State 过多时易状态爆炸、调试栈比 MVP 深。
三种模式对比
| 维度 | MVC | MVP | MVVM |
|---|---|---|---|
| 业务逻辑位置 | Activity | Presenter | ViewModel |
| View 与逻辑解耦 | 弱 | 强 | 强 |
| 是否可单测 | 几乎不可 | 可(Mock View) | 可(观察 LiveData) |
| View 引用 | 自身 | Presenter 持有 View 接口 | 不持有 |
| 配置变更处理 | 重新创建丢失状态 | 手动保留 Presenter | ViewModel 自动保留 |
| 通信方式 | 直接方法调用 | 接口回调 | 数据绑定 / LiveData 观察 |
| 学习成本 | 低 | 中 | 中高 |
| 推荐场景 | demo / 极简页面 | 中大型项目、强可测性 | Jetpack 项目、数据驱动 UI |
选型建议:
- 极小 demo / 工具页可用 MVC;
- 老项目无 Jetpack 依赖、需要可测性,可用 MVP;
- 新项目基于 AndroidX,推荐 MVVM + ViewModel + LiveData,后续可平滑过渡到 Compose。
常见坑与最佳实践
- MVP 中忘记 detachView:Activity 销毁时若 Presenter 仍持有 View 引用,会引发内存泄漏与崩溃,务必在
onDestroy()中调用detachView(),并在异步回调里判空。 - MVVM 中 ViewModel 持有 View 引用:永远不要把 Activity / Fragment 传给 ViewModel,只能暴露 LiveData 让 View 订阅。
- LiveData 在 ViewModel 中暴露为可变:对外只暴露
LiveData<T>,内部用MutableLiveData<T>,避免外部意外setValue。 - MVVM 状态爆炸:多个 LiveData 表示同一组相关状态时,建议合并为一个
UiState数据类,避免不一致。 - MVP 的 Contract 接口过细:方法粒度过细会让接口爆炸,按 UI 区块聚合而非按控件逐条暴露。
- 架构教条主义:不要为了架构而架构,小型页面过度拆分反而增加复杂度。
存量项目维护要点:架构演进路径
维护存量项目时,不要急于推翻重构,而应遵循“渐进式现代化”原则:
- 识别现状:先用第 36 章的调试与日志手段摸清现有架构边界,区分 MVC/MVP/MVVM 混用情况
- Strangler 模式:新功能用 MVVM + ViewModel + LiveData 实现,老功能保留原样,通过统一接口对外暴露,逐步“绞杀”老代码
- 引入 ViewModel:优先把 Activity/Fragment 中的业务逻辑抽到 ViewModel,这是最低成本、最高收益的一步
- LiveData 先行:在 Java 项目中 LiveData 可直接使用(androidx.lifecycle 依赖),无需等 Kotlin 迁移
- 避免大爆炸式重构:一次只改一个模块,保证可编译可测试,用第 30 章的单元测试守住行为不变
关键原则:架构演进的目的是让代码可维护,而不是追求“最新模式”。一个有测试覆盖的 MVC 模块,比一个没有测试的 MVVM 模块更安全。
章节小结
本章对比了 Android 中 MVC、MVP、MVVM 三种主流架构模式:MVC 把 Activity 当作 View + Controller 容易膨胀;MVP 通过 Presenter 与 Contract 接口实现解耦和可测性;MVVM 通过 ViewModel + LiveData 实现数据驱动 UI,天然规避 View 引用并支持配置变更自动恢复。三种模式没有绝对优劣,选型应基于项目规模、可测性需求与技术栈。
下一章预告
下一章将深入 ViewModel + LiveData + Lifecycle 的细节,讲解 ViewModel 作用域、LiveData 转换(Transformations.map / switchMap)、MediatorLiveData 与生命周期感知观察机制,并实战一个数据加载场景。