A
第 26 章JAVA35 分钟

架构模式: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。

常见坑与最佳实践

  1. MVP 中忘记 detachView:Activity 销毁时若 Presenter 仍持有 View 引用,会引发内存泄漏与崩溃,务必在 onDestroy() 中调用 detachView(),并在异步回调里判空。
  2. MVVM 中 ViewModel 持有 View 引用:永远不要把 Activity / Fragment 传给 ViewModel,只能暴露 LiveData 让 View 订阅。
  3. LiveData 在 ViewModel 中暴露为可变:对外只暴露 LiveData<T>,内部用 MutableLiveData<T>,避免外部意外 setValue。
  4. MVVM 状态爆炸:多个 LiveData 表示同一组相关状态时,建议合并为一个 UiState 数据类,避免不一致。
  5. MVP 的 Contract 接口过细:方法粒度过细会让接口爆炸,按 UI 区块聚合而非按控件逐条暴露。
  6. 架构教条主义:不要为了架构而架构,小型页面过度拆分反而增加复杂度。

存量项目维护要点:架构演进路径

维护存量项目时,不要急于推翻重构,而应遵循“渐进式现代化”原则:

  1. 识别现状:先用第 36 章的调试与日志手段摸清现有架构边界,区分 MVC/MVP/MVVM 混用情况
  2. Strangler 模式:新功能用 MVVM + ViewModel + LiveData 实现,老功能保留原样,通过统一接口对外暴露,逐步“绞杀”老代码
  3. 引入 ViewModel:优先把 Activity/Fragment 中的业务逻辑抽到 ViewModel,这是最低成本、最高收益的一步
  4. LiveData 先行:在 Java 项目中 LiveData 可直接使用(androidx.lifecycle 依赖),无需等 Kotlin 迁移
  5. 避免大爆炸式重构:一次只改一个模块,保证可编译可测试,用第 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 与生命周期感知观察机制,并实战一个数据加载场景。