news 2026/10/23 1:13:16

Android MVP架构从入门到实践:三层职责、代码实现与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android MVP架构从入门到实践:三层职责、代码实现与避坑指南

只要你写过一段时间的Activity,应该都遇到过这种场景:UI逻辑、业务逻辑、数据请求全堆在一个类里,一个页面文件上千行,改一个需求牵一发动全身。MVP架构就是用来收拾这种局面的。这篇文章面向完全没接触过MVP的初学者,也适合已经用但说不清原理的人,我会把架构的来龙去脉、三层职责、手写实现步骤、常见坑位一次讲清楚,内容全部基于我实际项目里的经验,不是教科书式说教。

网上关于架构的讨论一直很热,从微服务到分布式,从AI Agent到各种源码剖析,但回到移动端开发本身,MVP依然是很多老项目的中流砥柱,也是理解其他架构的一把钥匙。学会MVP,你再看MVVM、MVI都会轻松很多,因为它们本质都在解决同一件事:怎么让代码的职责更清楚、更好维护。

1. MVP架构到底在解决什么问题

先说一个扎心的事实:Android开发里,Activity/Fragment本身就承担了太多角色。它既负责界面展示,又负责响应点击事件,还要自己去拉数据、处理结果、更新UI。早期项目几乎都是这个套路,写起来确实爽,页面出来快,但项目一旦过三个月,你就开始后悔了。

1.1 传统MVC在Android上的困境

MVC大家都听过,Model-View-Controller。在Android里,很多人把Activity当成Controller,把XML当成View,Model就是数据层。看起来挺合理,但实际写下来你会发现,Activity才是真正的大管家,XML只是被Activity操控的木偶,Controller和View揉在一起,根本切不开。

带来的问题非常具体。第一,Activity生命周期复杂,onCreate、onResume、onDestroy里塞满了各种逻辑,稍不留神就会在页面销毁后还在回调里操作UI,直接崩溃。第二,业务逻辑没法单独测。你想验证一个登录逻辑对不对,得先把App跑起来,手动点按钮,眼睛盯着页面看结果,这效率太低了。第三,代码复用几乎为零。同一个业务逻辑换个页面就得复制粘贴一份,改一处漏一处,线上Bug就是这么来的。

我自己经历过一个维护两年的老项目,一个订单详情页八千多行,里面混杂着网络请求、数据库读写、倒计时、动画、埋点、权限申请,每次需求评审会看到这张页面的改动点,心里都发怵。这才下定决心把MVP正式引入项目。

1.2 MVP的核心思路与取舍

MVP的拆法比MVC更彻底:把View和Model彻底隔开,中间加一个Presenter作为桥梁。View只负责界面展示和事件转发,Model只负责数据和业务规则,Presenter负责接收View的指令、调用Model、再把结果回传给View。

用一句话概括:View变笨,Model变纯,Presenter变忙。

这样做的好处是立竿见影的。View层的代码量大幅缩减,一个Activity就剩下界面初始化和事件转发,很好读。业务逻辑集中在Presenter里,脱离了Android组件的束缚,可以用纯Java/Kotlin写单元测试,跑起来非常快。Model层可以独立演进,换数据源、改接口,只要保持接口不变,上层完全无感。

但MVP也不是银弹。它最常被吐槽的点是接口类爆炸,每个页面都对应一套View接口和Presenter接口,小项目会觉得啰嗦。另外Presenter变成纯逻辑类之后,如果你不注意管理View引用,内存泄漏的风险比MVC还大。这些坑我在后面专门讲。

2. MVP三层的职责边界与协作机制

知道MVP是什么之后,最关键的是搞清楚每一层到底管什么、不管什么。很多新手照着示例写,写着写着就变味了,最常见的就是把Presenter当成新的垃圾桶,逻辑全往里塞,结果Activity是瘦了,Presenter又胖了,问题一点没解决。

2.1 Model、View、Presenter各管什么

先给一个特别直白的界定。

View层就是Activity、Fragment、自定义View这些,它只干三件事:把界面画出来,把用户操作告诉Presenter,把Presenter传回来的结果显示出来。除此之外,什么都不用管。不需要关心数据从哪来,不需要关心登录成功之后要做什么业务处理。

Model层就是数据源和业务规则。数据源包括网络接口、本地数据库、SharedPreferences、文件等。业务规则指那些和界面无关的逻辑,比如计算金额、拼接字符串、校验输入格式、判定某个状态能否流转。这部分应该是整个App里最稳定的部分,不受UI变化影响。

Presenter层是核心协调者。它从View拿到事件,对应的数据操作叫Model去做,数据回来之后决定View该怎么展示。它还承担状态管理的责任,比如按钮是否可点、Loading是否显示、空页面还是错误页面。注意,Presenter不直接操作任何View控件,它只是通过View接口的方法去通知View改变。

2.2 接口在MVP里为什么不可或缺

MVP最让新手疑惑的一个点是:为什么非要搞那么多接口?我直接在Presenter里持有Activity的引用,调方法不行吗?不行,而且很危险。

直接持有Activity引用,会让Presenter和具体的Activity强耦合,换一个页面就没法复用。更严重的是,Activity销毁之后Presenter还在跑,它脑子里还拽着一个已经destroyed的Activity引用,一回调就是经典的"Leaked window"或者NullPointerException。

用接口解耦之后,Presenter只认识View接口,不关心背后是不是某个Activity,这就像你插座只认两孔或三孔的规格,不关心背后是哪家电厂的电。这样Presenter可以很轻松地离开Android环境跑单元测试,也可以让多个View共用一套业务逻辑,比如手机和平板两种布局分别写两个Activity,各自实现同一个View接口,Presenter完全不用改。

2.3 每一层的“铁律”

这一节是我带新人时反复强调的红线。Model层绝对不能出现Android的UI类,哪怕是Handler、Toast都不行,否则这个Model就没法脱离环境测试。View层绝对不允许出现业务判断,比如不让写if(user != null && permission == GRANTED)这种逻辑,只允许调用Presenter方法,展示Presenter给的最终结果。Presenter层绝对不允许持有View的强引用,后面细说。

这三条铁律守住,项目的架构就不会乱到哪里去。我在代码评审里看到最经典的违规就是新手在Fragment的onClick里写网络请求,然后直接更新TextView,这等于把MVP又写回了MVC。

3. 从零手写一个MVP登录场景

讲再多都不如写一遍。我下面用一个非常经典的登录页面来演示MVP的手写全过程,从接口定义到真正跑通,不依赖任何框架库,只用原生Java,你看完就能在自己项目里复刻。

3.1 场景设计与接口定义

需求很简单:页面有一个用户名输入框、一个密码输入框、一个登录按钮、一个状态显示文本。点击登录后校验输入,模拟网络请求,成功就显示欢迎语,失败就显示错误提示。

按照MVP的思路,第一步是先定义View接口和Presenter接口,通常叫Contract。这个命名方式来自Google官方样例,你会在很多公司项目里看到类似的写法。

public interface LoginContract { interface View { void showLoading(); void hideLoading(); void onLoginSuccess(String userName); void onLoginFail(String errorMsg); } interface Presenter { void login(String userName, String password); void detachView(); } }

View接口里只有展示状态相关的方法,没有任何业务语义。showLoading和hideLoading不是必须的,但实际项目中你一定会遇到,所以提前放进接口里。Presenter接口就一个登录动作和释放操作。不要一上来就把几十个方法塞进接口,先从小而美的场景起步,后面再逐步扩展。

第二步是写Model层。这里我用一个接口加一个实现类来模拟数据源。熟练之后你会觉得这一步可以精简,但对新手来说,Model独立成类是理解分层的好方式。

public interface LoginModel { void login(String userName, String password, Callback callback); interface Callback { void onSuccess(String userName); void onFailure(String errorMsg); } } public class LoginModelImpl implements LoginModel { @Override public void login(String userName, String password, Callback callback) { // 模拟网络请求延时 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } if ("admin".equals(userName) && "123456".equals(password)) { callback.onSuccess(userName); } else { callback.onFailure("用户名或密码错误"); } } }).start(); } }

这里的Model层完全不知道外界是谁在调用它,它只负责执行登录规则并回调结果。用户名密码校验写在Model里是比较合理的,因为这是一条业务规则,和页面无关。注意我用的是Thread模拟异步,真实项目你大概率会换成OkHttp、Retrofit或者协程,但MVP结构的组织方式是一样的。

3.2 Presenter实现核心逻辑

Presenter是整条链路的中枢。它持有View接口的引用,也持有Model的引用。用户在页面上点了登录按钮,View把参数丢给Presenter,Presenter先做基本的空判断,然后通知View显示Loading,再让Model干活。

public class LoginPresenter implements LoginContract.Presenter { private final LoginContract.View mView; private final LoginModel mModel; public LoginPresenter(LoginContract.View view) { mView = view; mModel = new LoginModelImpl(); } @Override public void login(String userName, String password) { if (TextUtils.isEmpty(userName) || TextUtils.isEmpty(password)) { mView.onLoginFail("用户名和密码不能为空"); return; } mView.showLoading(); mModel.login(userName, password, new LoginModel.Callback() { @Override public void onSuccess(String userName) { mView.hideLoading(); mView.onLoginSuccess(userName); } @Override public void onFailure(String errorMsg) { mView.hideLoading(); mView.onLoginFail(errorMsg); } }); } @Override public void detachView() { // 这里暂时留空,后面讲内存泄漏时细说 } }

仔细看这段代码,Presenter从头到尾没有碰过任何一个EditText、Button或者TextView,它只通过mView接口去通知页面。这样做最大的价值在于:LoginPresenter可以脱离Android环境直接测试,传一个mock的View进去,就能验证各种登录场景下View收到的回调是否符合预期,根本不用开着模拟器跑。

有一个细节值得注意,空判断放在Presenter而不是View层,这一点经常被讨论。我的习惯是基础的空字符串判断放Presenter没问题,因为它是业务入口处的参数校验。如果涉及更复杂的输入格式校验,比如手机号正则、邮箱格式,可以下沉到Model层或者专门的校验工具类,这样两边都能复用。

3.3 View层与Presenter绑定

Activity实现LoginContract.View接口,在onCreate里创建Presenter并把this传进去。然后所有的事件转发都交给Presenter。

public class LoginActivity extends AppCompatActivity implements LoginContract.View { private EditText etUserName; private EditText etPassword; private TextView tvStatus; private LoginContract.Presenter mPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); etUserName = findViewById(R.id.etUserName); etPassword = findViewById(R.id.etPassword); tvStatus = findViewById(R.id.tvStatus); mPresenter = new LoginPresenter(this); findViewById(R.id.btnLogin).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mPresenter.login(etUserName.getText().toString(), etPassword.getText().toString()); } }); } @Override public void showLoading() { tvStatus.setText("登录中..."); } @Override public void hideLoading() { tvStatus.setText(""); } @Override public void onLoginSuccess(String userName) { tvStatus.setText("欢迎回来," + userName); } @Override public void onLoginFail(String errorMsg) { tvStatus.setText(errorMsg); } @Override protected void onDestroy() { super.onDestroy(); mPresenter.detachView(); } }

这个页面现在非常轻,每个方法都只做一件事,可读性很强。你拿到这个Activity的代码,三分钟就能看出它的结构。事件流也很直观:点击按钮,参数交给Presenter,Presenter内部走Model回调,最后通过接口方法刷新界面。

到这里,一个完整的最小MVP闭环就形成了。我建议你把这段代码亲手敲一遍,不是复制粘贴,是一个字母一个字母敲,把手感练出来。然后再尝试加一两个功能,比如记住密码、显示密码明文切换,你会发现加功能变得很从容。

4. 框架化与演进:从手写MVP到工程化落地

手写一遍之后,你会觉得MVP其实不复杂,但真实项目里你不可能每个页面都从零写这些样板代码。工程化落地需要考虑批量生产的效率,这时候就该让框架登场了。

4.1 Google官方todo-mvp示例怎么读

Google在Android架构蓝图里开源了一套todo-mvp示例,这是我看过最适合新手进阶的样例。它不是普通的小Demo,而是一个完整的待办事项App,包含列表、编辑、详情、统计多个页面,并且用纯手工方式实现了MVP,没有任何第三方注入框架。

读这个示例有个技巧:先看整体包结构,你会发现每个功能模块都有一个Contract接口、一个Fragment负责View、一个Presenter类。包结构已经告诉了你代码组织的范式。然后重点看Presenter的调度逻辑,尤其是它如何处理异步回调的取消问题,这是MVP工程化里最微妙的部分。

todo-mvp还展示了MVP的另一个变体:让Fragment充当View,Activity只负责创建Fragment和Presenter。这种做法比我们上面演示的Activity直做View的写法更适合大型项目,因为Fragment的可复用性更好,一个页面可以适配手机和平板两种形态。但代价是Fragment的生命周期更复杂,新手容易在这里翻车。

4.2 引入RxJava、协程后的MVP

手写MVP跑通之后,你要面对的下一个现实问题是:真实项目的网络请求不会像示例里那样用Thread模拟,而会涉及线程切换、生命周期管理、异常链传递。这时候MVP依然成立,但实现细节要升级。

以RxJava为例,Model层可以把网络请求包装成Observable,Presenter负责订阅并处理结果。关键点是订阅关系的生命周期管理,通常通过CompositeDisposable来收集所有订阅,在detachView时统一dispose,这样异步回调就不会打到已经销毁的View上。

如果是Kotlin项目,协程是更现代的选择。我喜欢用ViewModel加协程再加LiveData来管理异步任务,但Presenter的模式仍然可以保留。具体思考方式不变:View只负责最终状态的展示,业务流全部往上层收拢。实际上很多团队从MVP向MVVM迁移时,发现思路是一脉相承的,变换的只是通信机制。

4.3 MVP和MVVM、MVI怎么选

这几年MVVM和MVI越来越流行,经常有新人问:既然它们更新,是不是可以直接学MVVM跳过MVP?我的回答是:先懂MVP,再决定用什么。MVP把"职责分层"这个概念讲得最透,你理解它之后,再看MVVM中ViewModel和DataBinding的分工,或者MVI中Intent、State、Effect的单向数据流,都会觉得似曾相识。

MVVM相比MVP最大的变化是用ViewModel替代Presenter,而且ViewModel有框架级的生命周期管理,可以自动感知页面销毁,这就解决了我前面提到的一大半泄漏问题。加上LiveData、Flow这些数据流组件,View层可以声明式地订阅状态,代码会更简洁。

MVI则走得更远,它把整个页面的所有状态合并成一个不可变的State对象,View单向接收State,用户的每个操作都变成一个Intent发给ViewModel,再由Reducer生成新的State。这种模式的调试体验极好,因为整个页面的状态快照都可以完整记录,回溯问题非常高效。

但从成本角度看,如果你的项目已经是成熟的MVP结构,没有必要强行迁移。架构选型看团队和业务,不追新。

5. 新手最容易踩的坑与排查经验

MVP写起来不难,难在写对。我见过太多项目挂着MVP的名,写着MVC的肉,也见过框架搭得很漂亮、但实际开发被各种隐藏问题折腾到崩溃的团队。下面这几个坑,基本是新手期一定会遇到的,我按实战经验给你排一遍雷。

5.1 内存泄漏:Presenter持有View引用

这绝对排行第一。MVP里Presenter持有View接口引用,这是设计需要,但如果不及时释放,Activity销毁后Presenter仍然被某个异步任务持有,那么Activity就永远无法被GC回收,内存泄漏就发生了。

解决方案说起来简单,做起来要养成习惯。第一,在Activity/Fragment的onDestroy里回调Presenter的detachView方法,把mView置为null。第二,所有异步任务在detach之后要主动取消,RxJava用dispose,协程用cancel,callback模式要在回调入口判断mView == null。我在前面代码里留空的detachView方法,就是让你补上mView = null这一步,这是整个MVP模式里最容易漏掉又最关键的一行代码。

这里要特别提醒:不要在onDestroyView里直接new一个Presenter,也不要让Presenter里的异步任务持有Activity的Context,改用Application级别的Context或者更优雅的方式是让Model层完全脱离Context依赖。

5.2 View层过重与Presenter膨胀

有些同学理解了分层之后,把所有代码拼命往外搬,结果从一个极端走到另一个极端。第一种极端是View层仍然做太多事,比如在Activity里去解析JSON、组装展示模型、处理分支逻辑,Presenter就变成一个传话的壳子,这种架构是死的。第二种极端正好相反,Presenter里什么都塞,包括构建Intent、跳转页面、弹Toast。你要知道,跳转和弹Toast属于UI导航行为,应该由View来执行,Presenter只负责告诉View"该跳转了"或者"该弹提示了"。

我判断一个分层是否健康,有一个很朴素的测试方法:把View层换成网页,把Presenter层拆出来独立测试,看逻辑还能不能跑通。如果跑不通,说明UI相关的逻辑泄到了Presenter里;如果跑得很顺畅,说明分层是干净的。你在自己项目里可以试试用MockView去驱动一下Presenter的单元测试,打卡你能覆盖多少业务场景。

5.3 Activity重建与状态恢复

Android系统会在内存不足或屏幕旋转时销毁并重建Activity,如果你只在onCreate里new了Presenter,重建之后旧的Presenter可能还挂在原来的异步任务上,新Presenter又要重新请求一次数据。这个问题的本质是Presenter的生命周期没有和Activity的销毁解耦。

常规做法是把Presenter的创建和恢复交出去。轻量方案是在onSaveInstanceState里保存关键状态,重建时恢复并让Presenter重新拉取数据。进阶方案是使用Google官方的ViewModelStore来保存Presenter实例。其实很多人就是从这个痛点开始转向MVVM的,因为ViewModel本身就是为生命周期设计而生的。

5.4 常见问题速查表

最后把我带新人时经常被问到的问题整理成一张表,你在自己项目里碰到类似现象可以直接对照排查。

现象原因解决方案
页面销毁后异步回调崩溃Presenter持有View强引用未释放detachView中置空,回调前判空
Activity泄漏异步任务持有Activity或Context使用ApplicationContext,任务统一取消
Presenter里拿不到界面控件错误地在Presenter操作UI通过View接口方法让Activity自己更新UI
View接口方法越来越多把一次性回调全塞进了长生命周期接口细分接口或者按场景拆分Contract
Fragment重建后UI不刷新Presenter状态丢失用ViewModel保存Presenter或用SavedState恢复
页面里网络请求逻辑满天飞没有严格分层按MVP铁律重构,先Pull Request评审

这张表里的每一条,都是我在真实项目里排查过的,不是凭空想的。你如果严格按照MVP的规则去写,这张表里的大多数问题根本不会出现。它之所以存在,就是因为开发者在写的过程中不断"灵活"处理,灵活到最后架构就塌了。

我个人在实际操作中的体会是,MVP不是一种高深的技术,它更像一种纪律。你不需要理解特别复杂的理论,但需要保持克制:View只做展示,Model只做数据,Presenter只做协调。这条线一旦划清楚,后面所有需求迭代都变得很有节奏感。另外还有一个小技巧,接手别人MVP项目的时候,先别急着看代码,把每个Contract接口打开,看每个方法是不是符合职责边界,这套体检做完,项目质量大概心里就有数了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 1:12:33

IDEA 2025.3.3 新建 Maven 项目:JDK、镜像与依赖配置

如果你刚装好 IDEA 2025.3.3,想从零建一个 Maven 项目,大概会跟我第一次上手时一样:跟着网上的老教程点了半天,发现按钮位置完全对不上,好不容易建完项目,一运行又是依赖下载超时、JDK 报错。这个版本的新项…

作者头像 李华
网站建设 2026/10/23 1:12:38

PyTorch CyclicLR循环学习率:原理、参数与实战指南

1. 先搞懂一个反直觉的现象:Loss长期不动,问题可能不在模型而在学习率有段时间我在PyTorch里训练一个图像分类模型,loss卡在0.7附近怎么也下不去。学习率从0.1一路调到0.01、再到0.003,曲线要么震荡要么干脆不动。后来换成CyclicL…

作者头像 李华
网站建设 2026/10/16 23:47:35

行人目标检测数据集与YOLOv5目录格式转换实战指南

简介:行人目标检测数据集(YOLOV5目录格式)面向目标检测开发者和入门学习者,聚焦生活场景中的单一类别person识别,可应用于安防监控、智能驾驶、人流量统计等任务,尤其适合需要快速搭建行人检测训练集的学生…

作者头像 李华
网站建设 2026/10/16 7:41:18

智能网关实现串口设备快速上以太网,PLC免编程统一监控方案

在自动化现场摸爬滚打的朋友,十有八九都遇到过这种局面:主控柜里一台PLC,现场却散落着变频器、智能仪表、温控表、称重仪表,清一色RS485串口。想把这些串口设备的数据全部并到主PLC里做统一监控,传统做法要么拉一大把屏…

作者头像 李华
网站建设 2026/10/21 20:27:18

Blackfire探针未响应?从组件链路到权限问题一次讲清

上周帮一个同事排查性能问题,他在本地环境跑blackfire curl,控制台直接甩出一句:Are you authorized to profile this page? No probe response, Blackfire not properly installed or invalid这句话信息量很大,但又一环都没说明…

作者头像 李华
网站建设 2026/10/17 1:57:02

管家婆软件安装遇“修改修复删除”界面?残留清理与重装全攻略

帮客户装管家婆软件的时候,我经常碰到这样一种情况:安装包双击下去,没进入“下一步下一步”的正常流程,反而弹出一个界面让你“选择产品版本”,或者干脆出现“修改、修复、删除”三个选项。第一眼看上去确实懵——我装…

作者头像 李华