在 Android 开发中,我们经常会听到 MVC、MVP 和 MVVM。它们并不是某个框架或库,而是组织界面、业务逻辑与数据的一组思想。一个项目刚开始时,所有代码都写在 Activity 里似乎最省事;随着页面变多、需求反复变化,网络请求、状态判断、控件更新和生命周期处理混在一起,Activity 很快就会膨胀到上千行。此时,代码不仅难读,也难测试、难复用,更容易因为一次局部修改影响整个页面。
架构模式的价值,就是给不同职责划清边界,让代码形成稳定的协作关系。MVC、MVP、MVVM 诞生于不同阶段,解决问题的方式也不同。本文将从 Android 的实际开发出发,分析三种模式的结构、代码实现、优缺点和适用场景,并说明现代 Android 项目为什么普遍更倾向于 MVVM。
一、为什么 Android 项目需要架构模式
假设我们要开发一个用户详情页:页面打开后根据用户 ID 请求接口,显示头像、姓名和简介;请求期间展示加载状态;失败时显示错误提示,并允许用户点击重试。看似简单的页面,至少涉及以下内容:
-
接收并校验用户 ID;
-
发起异步网络请求;
-
解析和保存用户数据;
-
管理加载、成功、失败等界面状态;
-
更新 TextView、ImageView 和进度控件;
-
处理屏幕旋转、页面销毁和请求取消;
-
编写测试,确认各种状态下行为正确。
如果所有逻辑都放在 Activity 中,Activity 既负责界面绘制,又负责业务决策,还直接操作网络和数据库。这种类通常被称为"上帝类":它知道得太多,也承担得太多。任何一部分变化都会迫使我们修改 Activity,而且大量逻辑依赖 Android 控件,普通单元测试很难覆盖。
合理的架构通常追求几个目标:关注点分离、低耦合、高内聚、便于测试,以及适应生命周期变化。关注点分离意味着界面只关心如何展示,业务层只关心做什么,数据层只关心从哪里获取数据;低耦合意味着一个模块的具体实现变化不会牵连所有模块;高内聚则要求同一类职责集中在一起,而不是散落在多个角落。
MVC、MVP 和 MVVM 都围绕三个核心角色展开:数据、界面和协调逻辑。它们真正的区别,在于"谁负责协调"和"界面如何得到新状态"。
二、MVC:最经典的分层思想
MVC 是 Model、View、Controller 的缩写。
-
Model 负责数据及业务规则,例如实体类、数据库访问、网络请求和 Repository。
-
View 负责呈现界面,并接收用户输入。
-
Controller 接收用户操作,调用 Model,再决定如何更新 View。
理想的调用流程是:用户操作 View,View 将事件交给 Controller;Controller 调用 Model 完成业务;Model 返回结果;Controller 再选择合适的 View 进行展示。三者各司其职,使数据、界面和控制逻辑不再全部堆在一起。
1. MVC 在 Android 中的特殊性
Android 并没有官方规定 Activity 必须对应 MVC 中的哪一个角色。XML 布局显然属于 View,但 Activity 和 Fragment 同时承担了控件绑定、事件监听、页面跳转和业务调度,因此经常既像 View,又像 Controller。正是这种角色重叠,让 Android 中的传统 MVC 很容易演变成"XML 是 View,Activity 包办其他一切"的结构。
以登录功能为例,Model 可以封装登录请求:
java
public class User {
private final long id;
private final String name;
public User(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() { return id; }
public String getName() { return name; }
}
public class UserRepository {
private final UserApi api;
public UserRepository(UserApi api) {
this.api = api;
}
public Result<User> login(String account, String password) {
if (account == null || account.trim().isEmpty()
|| password == null || password.trim().isEmpty()) {
return Result.failure(
new IllegalArgumentException("账号和密码不能为空"));
}
try {
User user = api.login(account, password);
return Result.success(user);
} catch (Exception e) {
return Result.failure(e);
}
}
}
Activity 作为 Controller,监听按钮并更新界面:
java
public class LoginActivity extends AppCompatActivity {
private ActivityLoginBinding binding;
private final UserRepository repository =
new UserRepository(ApiFactory.getUserApi());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.loginButton.setOnClickListener(v ->
login(
binding.accountEdit.getText().toString(),
binding.passwordEdit.getText().toString()
));
}
private void login(String account, String password) {
binding.progressBar.setVisibility(View.VISIBLE);
binding.loginButton.setEnabled(false);
// 使用协程或线程池执行异步请求
CoroutineScope scope = ...; // 略
scope.launch(() -> {
Result<User> result = repository.login(account, password);
runOnUiThread(() -> {
if (result.isSuccess()) {
User user = result.getOrNull();
startActivity(
HomeActivity.newIntent(this, user.getId()));
} else {
binding.errorText.setText(
result.exceptionOrNull().getMessage());
binding.errorText.setVisibility(View.VISIBLE);
}
binding.progressBar.setVisibility(View.GONE);
binding.loginButton.setEnabled(true);
});
return null;
});
}
}
这个例子已经比把请求细节也写进 Activity 更清晰:数据访问被放入 Repository,Activity 负责协调。但账号校验结果、加载状态、异常映射、页面跳转等控制逻辑仍然依赖 Activity。功能增加后,Activity 依旧会不断变大。
2. MVC 的优点
MVC 概念简单、学习成本低,很适合页面少、业务简单的应用。它不需要定义大量接口,也不依赖特定响应式框架。对于一次性工具页、原型项目或只有少量交互的页面,使用 MVC 能快速完成开发。Model 被单独抽离后,数据代码也能获得一定的复用能力。
3. MVC 的局限
Android 中的 View 与 Controller 很难彻底分开。Activity 需要直接访问控件,又要响应生命周期和系统回调,最终容易聚集大量代码。Controller 通常依赖真实的 Android 环境,单元测试必须借助 Robolectric 或仪器测试,执行慢、维护成本高。此外,Controller 主动更新多个控件时,界面状态容易分散:加载动画已经隐藏,但按钮可能忘记恢复;错误文字更新了,旧数据却没有清除。
所以 MVC 的主要问题并不是"不能用",而是在复杂 Android 页面中边界容易模糊,规模扩大后难以保持整洁。
三、MVP:让 Presenter 接管界面逻辑
MVP 是 Model、View、Presenter 的缩写。它是在 MVC 基础上的进一步解耦。Presenter 代替 Controller 成为中间协调者,并通过接口与 View 交互。
在 MVP 中:
-
Model 仍然负责数据和业务规则;
-
View 通常由 Activity 或 Fragment 实现,只处理控件展示与用户事件;
-
Presenter 持有 View 接口,响应事件、调用 Model,并命令 View 显示结果。
View 不直接操作 Model,Model 也不知道 View 的存在。Presenter 不应依赖 TextView、Context 等具体 Android 类型,因此大部分业务判断可以在 JVM 中进行快速单元测试。
1. 用 Contract 明确双方职责
MVP 项目通常先定义契约接口:
java
public interface LoginContract {
interface View {
void showLoading();
void hideLoading();
void showError(String message);
void openHome(long userId);
}
interface Presenter {
void login(String account, String password);
void detachView();
}
}
Presenter 负责流程控制:
java
public class LoginPresenter implements LoginContract.Presenter {
private LoginContract.View view;
private final UserRepository repository;
private final CoroutineScope scope;
public LoginPresenter(LoginContract.View view,
UserRepository repository,
CoroutineScope scope) {
this.view = view;
this.repository = repository;
this.scope = scope;
}
@Override
public void login(String account, String password) {
if (account == null || account.trim().isEmpty()
|| password == null || password.trim().isEmpty()) {
if (view != null) view.showError("请输入账号和密码");
return;
}
scope.launch(() -> {
if (view != null) view.showLoading();
Result<User> result = repository.login(account, password);
runOnUiThread(() -> {
if (result.isSuccess()) {
if (view != null) view.openHome(
result.getOrNull().getId());
} else {
String msg = result.exceptionOrNull() != null
? result.exceptionOrNull().getMessage()
: "登录失败";
if (view != null) view.showError(msg);
}
if (view != null) view.hideLoading();
});
return null;
});
}
@Override
public void detachView() {
view = null;
}
}
Activity 只实现界面操作:
java
public class LoginActivity extends AppCompatActivity
implements LoginContract.View {
private ActivityLoginBinding binding;
private LoginContract.Presenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
presenter = new LoginPresenter(
this,
new UserRepository(ApiFactory.getUserApi()),
new CoroutineScope() // 略
);
binding.loginButton.setOnClickListener(v ->
presenter.login(
binding.accountEdit.getText().toString(),
binding.passwordEdit.getText().toString()
));
}
@Override
public void showLoading() {
binding.progressBar.setVisibility(View.VISIBLE);
binding.loginButton.setEnabled(false);
}
@Override
public void hideLoading() {
binding.progressBar.setVisibility(View.GONE);
binding.loginButton.setEnabled(true);
}
@Override
public void showError(String message) {
binding.errorText.setText(message);
binding.errorText.setVisibility(View.VISIBLE);
}
@Override
public void openHome(long userId) {
startActivity(HomeActivity.newIntent(this, userId));
}
@Override
protected void onDestroy() {
presenter.detachView();
super.onDestroy();
}
}
Activity 不再决定登录条件和处理结果,它只负责"怎么显示"。Presenter 也不关心错误信息使用 Toast、TextView 还是弹窗展示,只调用接口表达意图。
2. MVP 为什么更容易测试
测试 Presenter 时,可以使用一个假的 View 记录调用结果,不需要启动 Activity:
java
public class FakeLoginView implements LoginContract.View {
public String error;
public Long openedUserId;
public boolean loading;
@Override
public void showLoading() { loading = true; }
@Override
public void hideLoading() { loading = false; }
@Override
public void showError(String message) { error = message; }
@Override
public void openHome(long userId) { openedUserId = userId; }
}
测试可以直接验证空账号是否触发错误、登录成功是否跳转,以及请求结束后是否关闭加载状态。Presenter 的输入和输出都很明确,这也是 MVP 曾在 Android 社区广泛流行的重要原因。
3. MVP 的优势与问题
MVP 将业务展示逻辑从 Activity 中抽离,职责比 MVC 清晰。Presenter 是普通 Java 类,测试成本低;View 接口还能够屏蔽 Android 控件的具体实现。对于传统 View 体系、交互复杂而状态数量有限的项目,MVP 仍然是一种可靠选择。
但它也有明显代价。每个页面通常需要 Contract、View、Presenter 和实现类,接口与样板代码很多。Presenter 通过 showLoading()、showContent()、showError() 等命令逐步操作 View,状态一多,接口会持续膨胀。更棘手的是生命周期:Presenter 持有 Activity 引用,如果页面销毁后没有及时解除绑定,可能造成内存泄漏;异步任务返回时 View 已不存在,也需要额外判断。
此外,MVP 的更新方式仍然是命令式的。Presenter 告诉界面依次执行若干动作,如果中间漏掉一步,最终界面可能处于不一致状态。现代开发更希望把整个页面描述为一个完整状态,由界面一次性渲染,这正是 MVVM 擅长的方向。
四、MVVM:以可观察状态驱动界面
MVVM 是 Model、View、ViewModel 的缩写。
-
Model 负责业务数据与数据来源;
-
View 是 Activity、Fragment 或 Compose 界面,只负责展示状态和上报事件;
-
ViewModel 保存页面状态、处理用户意图,并调用 Model 获得数据。
MVVM 与 MVP 最直观的区别是:Presenter 主动调用 View 接口,而 ViewModel 通常不持有 View。View 主动观察 ViewModel 暴露的状态,状态变化后自动刷新界面。因此,ViewModel 不需要知道页面具体由 Activity、Fragment 还是 Compose 实现。
Android Jetpack 提供的ViewModel可以在配置变更时保留数据,LiveData 能感知生命周期,Kotlin StateFlow 则提供了更统一的协程响应式能力。这些组件使 MVVM 与 Android 生命周期天然契合。
1. 用 UiState 表达完整页面
现代 MVVM 常使用一个不可变的数据类描述整个页面:
java
public final class LoginUiState {
private final String account;
private final String password;
private final boolean isLoading;
private final String errorMessage;
private final Long loginSuccessUserId;
public LoginUiState(String account,
String password,
boolean isLoading,
String errorMessage,
Long loginSuccessUserId) {
this.account = account;
this.password = password;
this.isLoading = isLoading;
this.errorMessage = errorMessage;
this.loginSuccessUserId = loginSuccessUserId;
}
public static LoginUiState empty() {
return new LoginUiState("", "", false, null, null);
}
// getters
public String getAccount() { return account; }
public String getPassword() { return password; }
public boolean isLoading() { return isLoading; }
public String getErrorMessage() { return errorMessage; }
public Long getLoginSuccessUserId() { return loginSuccessUserId; }
// copy 方法模拟 data class 的 copy
public LoginUiState copy(String account,
String password,
Boolean isLoading,
String errorMessage,
Long loginSuccessUserId) {
return new LoginUiState(
account != null ? account : this.account,
password != null ? password : this.password,
isLoading != null ? isLoading : this.isLoading,
errorMessage, // 允许置空
loginSuccessUserId
);
}
}
ViewModel 接收事件并产生新状态:
java
public class LoginViewModel extends ViewModel {
private final MutableStateFlow<LoginUiState> _uiState =
new MutableStateFlow<>(LoginUiState.empty());
public final StateFlow<LoginUiState> uiState = _uiState;
private final UserRepository repository;
public LoginViewModel(UserRepository repository) {
this.repository = repository;
}
public void onAccountChanged(String value) {
_uiState.setValue(_uiState.getValue()
.copy(value, null, null, null, null));
}
public void onPasswordChanged(String value) {
_uiState.setValue(_uiState.getValue()
.copy(null, value, null, null, null));
}
public void login() {
LoginUiState state = _uiState.getValue();
if (state.getAccount().trim().isEmpty()
|| state.getPassword().trim().isEmpty()) {
_uiState.setValue(state.copy(
null, null, null, "请输入账号和密码", null));
return;
}
// 使用 viewModelScope 或自定义协程作用域
getViewModelScope().launch(() -> {
_uiState.setValue(_uiState.getValue().copy(
null, null, true, null, null));
Result<User> result = repository.login(
state.getAccount(), state.getPassword());
runOnUiThread(() -> {
if (result.isSuccess()) {
_uiState.setValue(_uiState.getValue().copy(
null, null, false, null,
result.getOrNull().getId()));
} else {
String msg = result.exceptionOrNull() != null
? result.exceptionOrNull().getMessage()
: "登录失败";
_uiState.setValue(_uiState.getValue().copy(
null, null, false, msg, null));
}
});
return null;
});
}
public void onNavigationHandled() {
_uiState.setValue(_uiState.getValue().copy(
null, null, null, null, -1L)); // 或使用一个标志
}
private CoroutineScope getViewModelScope() {
// 略
return null;
}
}
Fragment 在合适的生命周期内收集状态:
java
viewLifecycleOwner.getLifecycleScope().launch(() -> {
// 使用 repeatOnLifecycle 或 collect 监听 StateFlow
viewModel.uiState.collect(state -> {
binding.progressBar.setVisibility(
state.isLoading() ? View.VISIBLE : View.GONE);
binding.loginButton.setEnabled(!state.isLoading());
binding.errorText.setVisibility(
state.getErrorMessage() != null ? View.VISIBLE : View.GONE);
binding.errorText.setText(state.getErrorMessage());
if (state.getLoginSuccessUserId() != null) {
findNavController().navigate(
LoginFragmentDirections.toHome(
state.getLoginSuccessUserId()));
viewModel.onNavigationHandled();
}
return null;
});
return null;
});
此时 ViewModel 完全不持有 Fragment。屏幕旋转导致 Fragment 重建时,同一个 ViewModel 中的状态仍然存在,新界面重新收集即可恢复显示。界面渲染也更加集中:看到 LoginUiState,就能知道页面在任意时刻可能具备哪些信息。
2. MVVM 与 Data Binding
早期 Android MVVM 经常和 Data Binding 一起出现。XML 可以直接绑定 ViewModel 的可观察字段,减少手动调用 setText() 或 setVisibility() 的代码。例如:
java
<ProgressBar
android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}" />
<Button
android:onClick="@{() -> viewModel.login()}"
android:enabled="@{!viewModel.loading}" />
但 Data Binding 并不是 MVVM 的必要条件。使用 View Binding 配合 StateFlow(或 LiveData),同样属于 MVVM;Jetpack Compose 通过状态触发重组,更是天然适合单向数据流。架构的核心是职责与数据流,而不是是否使用某个带有"Binding"字样的库。
3. MVVM 的优势
MVVM 最大的优点是 ViewModel 与 View 之间没有直接引用。ViewModel 能跨配置变更保存页面数据,也可以在普通 JVM 环境中测试。通过不可变 UiState,页面状态更集中、更容易追踪,复杂的加载、空数据、错误、刷新等组合也能被准确表达。
MVVM 还非常适合响应式开发。数据层可以返回 Flow,ViewModel 将多个数据源组合成 UiState,View 只订阅最终结果。当数据库内容变化时,状态链路会自动传播,不必让 Repository 回调 Presenter,再由 Presenter 逐个通知控件。
4. MVVM 的常见误区
第一,ViewModel 不是 Activity 的"代码垃圾桶"。如果把所有网络、数据库和复杂业务规则都塞进 ViewModel,它同样会膨胀。合理做法是引入 Repository 管理数据来源,必要时用 UseCase 封装可复用业务。
第二,不要让 ViewModel 持有 Activity、Fragment、View 或短生命周期 Context。需要 Context 的资源访问可以在 View 层完成,或谨慎使用 Application Context。页面跳转、权限申请、弹窗等界面行为应由 View 执行。
第三,要区分"状态"和"一次性事件"。用户资料是可重复读取的状态;Toast、导航、支付结果提示通常只应消费一次。如果把导航目标长期保存在 StateFlow 中,页面重建后可能重复跳转。可以在处理后清除状态,或根据团队约定使用 Channel、SharedFlow,但必须考虑事件在订阅者不活跃时是否允许丢失。
第四,不要在 XML 或 Composable 中塞入复杂业务判断。界面可以决定颜色和排版,却不应计算折扣规则、订单资格或权限策略。否则业务逻辑又会从 ViewModel 泄漏回 View。
五、从 MVC 迁移到 MVP、MVVM
架构升级不意味着必须重写整个项目。更稳妥的方法是围绕具体问题逐步拆分。
如果一个 MVC 页面中的 Activity 过大,可以先把网络和数据库代码抽到 Repository,再把校验、数据转换和状态判断移出 Activity。如果团队熟悉接口驱动和传统 View,可以创建 Presenter 与 View Contract,形成 MVP;如果项目已经使用 Jetpack、协程或 Compose,则更适合创建 ViewModel,并把零散控件变量合并成 UiState。
例如,旧 Activity 中可能有 loadUser()、updateUserView()、showError() 和 retry()。迁移到 MVVM 时,可以按以下顺序进行:
-
让 Repository 提供统一的用户数据获取方法;
-
创建 ViewModel,将加载逻辑移入 viewModelScope;
-
定义 Loading、Success、Error,或一个完整的 UserUiState;
-
Activity 只收集状态并渲染;
-
将按钮点击转换为 ViewModel 事件;
-
补充 ViewModel 单元测试,确认迁移前后行为一致。
迁移过程中最重要的是保持依赖方向清晰:View 可以依赖 ViewModel,ViewModel 可以依赖业务抽象或 Repository,但 Repository 不应该反过来依赖 ViewModel,更不能依赖某个 Activity。接口最好定义在使用方需要抽象的位置,并通过构造函数注入具体实现。
六、结语
MVC、MVP 和 MVVM 的共同目标,都是将数据、业务和界面分开。MVC 结构直接、实现轻量,但 Android 中 Activity 容易兼任 View 与 Controller;MVP 使用 Presenter 和 View 接口加强了解耦,显著改善了测试能力,却带来接口膨胀和生命周期管理问题;MVVM 通过 ViewModel 与可观察状态驱动界面,更适合 Jetpack、协程与 Compose 时代的 Android 开发。
真正优秀的架构并不取决于类的数量,也不取决于是否使用了最流行的缩写,而取决于职责是否清晰、数据流是否容易追踪、代码是否方便修改和验证。小项目应保持简单,大项目应提前建立边界;旧项目尊重已有成本,新项目则可以充分利用现代 Android 工具链。
当我们能够回答"这段逻辑属于谁""状态由谁保存""界面为什么发生变化"时,架构就已经发挥了作用。MVC、MVP 和 MVVM 不是必须背诵的标准答案,而是帮助开发者管理复杂度的三种思考方式。理解它们的差异,再结合项目规模、团队经验和技术栈作出选择,才是架构设计真正有价值的地方。