Java MVP案例

wen java案例 2

Java MVP模式实战:从零搭建可维护的Android架构(附完整案例)

目录导读

  • MVP架构核心思想:为什么Google官方推荐的MVVM当道,我们仍需掌握MVP?
  • 案例需求分析:一个用户登录模块如何贯穿Model、View、Presenter三层。
  • 代码逐层拆解:接口定义、Presenter生命周期绑定、内存泄漏防范。
  • 与经典MVC对比:一次点击事件背后的数据流差异。
  • 常见问题问答:关于线程切换、多Presenter复用、测试性的深度答疑。
  • 架构演进思考:MVP如何平滑过渡到MVVM,保留哪些核心资产。

在Android开发历史长河中,MVP(Model-View-Presenter) 架构曾是解决Activity“上帝类”问题的银弹,尽管如今Jetpack MVVM已成主流,但大量遗留项目维护、以及面试中对架构解耦能力的考察,使得MVP依然是Java工程师的必修课,本文通过一个登录模块的完整案例,剖析MVP的每一根筋骨,并为你解答实战中90%的坑。

Java MVP案例

MVP架构核心思想:UI逻辑与业务逻辑的“离婚协议”

MVP模式将界面(View)与数据(Model)彻底隔离,由Presenter充当“中间调解人”,其核心铁律:View不直接触碰Model,Model不持有View引用,这带来的直接好处是:

  1. 单元测试友好:JVM环境即可测试Presenter,无需模拟Android组件。
  2. UI复用性:同一个Presenter可服务于Activity、Fragment甚至自定义View。
  3. 职责单一:Activity只负责渲染与事件转发,不再混入网络请求或数据库逻辑。

为什么仍值得学? 在低版本Android设备或嵌入式场景,MVP比MVVM更轻量,且对DataBinding不友好项目是首选。

案例需求:用户登录模块

我们构建一个标准登录界面:用户名、密码输入框,一个登录按钮,要求登录成功后跳转主页,失败则显示Toast,这就是经典的“最小但五脏俱全”案例。

  • Model层:模拟网络IO,提供login(username, password)方法,返回成功或失败回调。
  • View层:暴露showLoading()hideLoading()onLoginSuccess()onLoginFailed(message)等方法。
  • Presenter层:持有View接口引用,调用Model执行登录,并回调View刷新UI。

代码逐层拆解:从接口到实现的每一行

定义契约接口(Contract)

这是MVP的“合同”,先定规矩,再写实现:

public interface LoginContract {
    interface View {
        void showLoading();
        void hideLoading();
        void onLoginSuccess();
        void onLoginFailed(String message);
    }
    interface Presenter {
        void login(String username, String password);
        void onDestroy(); // 释放引用
    }
    interface Model {
        void login(String username, String password, Callback callback);
        interface Callback {
            void onSuccess();
            void onFailure(String error);
        }
    }
}

Model实现:以假乱真的网络模拟

public class LoginModel implements LoginContract.Model {
    @Override
    public void login(String username, String password, Callback callback) {
        // 此处仅模拟,可替换为Retrofit或OkHttp请求
        new Thread(() -> {
            try {
                Thread.sleep(1500); // 模拟网络延迟
                if ("admin".equals(username) && "123456".equals(password)) {
                    callback.onSuccess();
                } else {
                    callback.onFailure("用户名或密码错误");
                }
            } catch (InterruptedException e) {
                callback.onFailure("网络异常");
            }
        }).start();
    }
}

View实现(Activity):只做UI操作

public class LoginActivity extends AppCompatActivity implements LoginContract.View {
    private EditText etUsername, etPassword;
    private ProgressBar progressBar;
    private LoginContract.Presenter presenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        // 初始化控件...
        presenter = new LoginPresenter(this, new LoginModel());
        findViewById(R.id.btn_login).setOnClickListener(v -> {
            presenter.login(etUsername.getText().toString(), etPassword.getText().toString());
        });
    }
    @Override
    public void showLoading() { progressBar.setVisibility(View.VISIBLE); }
    @Override
    public void hideLoading() { progressBar.setVisibility(View.GONE); }
    @Override
    public void onLoginSuccess() { Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show(); }
    @Override
    public void onLoginFailed(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); }
    @Override
    protected void onDestroy() {
        presenter.onDestroy(); // 关键:防止内存泄漏
        super.onDestroy();
    }
}

Presenter实现:逻辑编排的“大脑”

public class LoginPresenter implements LoginContract.Presenter {
    private final LoginContract.View view;
    private final LoginContract.Model model;
    public LoginPresenter(LoginContract.View view, LoginContract.Model model) {
        this.view = view;
        this.model = model;
    }
    @Override
    public void login(String username, String password) {
        // 空值校验
        if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {
            view.onLoginFailed("输入不能为空");
            return;
        }
        view.showLoading();
        model.login(username, password, new LoginContract.Model.Callback() {
            @Override
            public void onSuccess() {
                if (view != null) { // 防御性检查
                    view.hideLoading();
                    view.onLoginSuccess();
                }
            }
            @Override
            public void onFailure(String error) {
                if (view != null) {
                    view.hideLoading();
                    view.onLoginFailed(error);
                }
            }
        });
    }
    @Override
    public void onDestroy() {
        view = null; // 解除引用,避免Activity泄漏
    }
}

核心代码细节:Presenter中的onDestroy()将View置空,是因为子线程回调可能晚于Activity销毁,若不加处理,Java对象持有Activity引用,导致内存泄漏——这是MVP最经典的坑。

与经典MVC对比:一次点击事件的数据流差异

在MVC中,Activity既是Controller又是View,登录逻辑直接写在Activity里,导致Activity臃肿不堪,而MVP中:

  • MVC:点击 → Activity调Model → Model回调Activity → Activity改UI(Activity承担过多)。
  • MVP:点击 → View调Presenter → Presenter调Model → 回调Presenter → Presenter调View改UI。

中间多了Presenter这一层,让View和Model彻底“断联”,这就是单元测试的突破口

常见问题问答(实战避坑指南)

Q1:Presenter中的线程切换如何处理? A:案例中Model层用Thread模拟,真实项目推荐使用RxJava或协程,但核心是回调必须回到主线程,若用Retrofit,其内部自动切回主线程;若手写线程,用HandlerrunOnUiThread包装回调。

Q2:多个Activity共用同一个Presenter怎么办? A:将Presenter抽成抽象基类,或使用接口隔离,例如定义BasePresenter<T extends BaseView>,内置attachViewdetachView方法,子类只需关注业务方法。

Q3:MVP如何做单元测试? A:纯Java测试只需要Mock一个View接口(用Mockito),创建Presenter实例,调用方法,验证View回调是否正确,完全不需要启动Android模拟器,这是MVP最大的优势。

Q4:MVP中View接口过于庞大怎么办? A:按业务域拆分View接口,比如登录模块只定义LoginContract.View,不要搞一个包含所有UI方法的“万能View接口”。

架构演进思考:MVP → MVVM平滑过渡

MVP的痛点在于Presenter在Activity销毁后仍需手动防止泄漏,且View层与Presenter交互频繁,若项目想迁移到MVVM:

  1. 保留Model层,这是业务逻辑的稳定部分。
  2. 将Presenter的调度职责交给ViewModel + LiveData
  3. View中的回调逻辑改为观察LiveData,Activity只做数据绑定。

但无论架构如何演变,“界面与数据分离” 的魂始终贯穿,MVP的接口驱动思想,正是MVVM中Repository模式的前身。


MVP不是一个完美架构,但却是理解Android一切“解耦哲学”的基石,掌握本文的登录案例,你不仅能维护老旧项目,更能在理解其局限后,更通透地使用MVVM,动手将代码跑一遍,亲手断点追踪一次数据流,胜过阅读十遍概念。

抱歉,评论功能暂时关闭!