恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Android MVP架构从入门到实践:三层职责、代码实现与避坑指南
首页
资讯中心
/
Android MVP架构从入门到实践:三层职责、代码实现与避坑指南
Android MVP架构从入门到实践:三层职责、代码实现与避坑指南
发布时间:2026/10/9 8:38:28
只要你写过一段时间的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当成ViewModel就是数据层。看起来挺合理但实际写下来你会发现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把参数丢给PresenterPresenter先做基本的空判断然后通知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的代码三分钟就能看出它的结构。事件流也很直观点击按钮参数交给PresenterPresenter内部走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充当ViewActivity只负责创建Fragment和Presenter。这种做法比我们上面演示的Activity直做View的写法更适合大型项目因为Fragment的可复用性更好一个页面可以适配手机和平板两种形态。但代价是Fragment的生命周期更复杂新手容易在这里翻车。4.2 引入RxJava、协程后的MVP手写MVP跑通之后你要面对的下一个现实问题是真实项目的网络请求不会像示例里那样用Thread模拟而会涉及线程切换、生命周期管理、异常链传递。这时候MVP依然成立但实现细节要升级。以RxJava为例Model层可以把网络请求包装成ObservablePresenter负责订阅并处理结果。关键点是订阅关系的生命周期管理通常通过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协程用cancelcallback模式要在回调入口判断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自己更新UIView接口方法越来越多把一次性回调全塞进了长生命周期接口细分接口或者按场景拆分ContractFragment重建后UI不刷新Presenter状态丢失用ViewModel保存Presenter或用SavedState恢复页面里网络请求逻辑满天飞没有严格分层按MVP铁律重构先Pull Request评审这张表里的每一条都是我在真实项目里排查过的不是凭空想的。你如果严格按照MVP的规则去写这张表里的大多数问题根本不会出现。它之所以存在就是因为开发者在写的过程中不断灵活处理灵活到最后架构就塌了。我个人在实际操作中的体会是MVP不是一种高深的技术它更像一种纪律。你不需要理解特别复杂的理论但需要保持克制View只做展示Model只做数据Presenter只做协调。这条线一旦划清楚后面所有需求迭代都变得很有节奏感。另外还有一个小技巧接手别人MVP项目的时候先别急着看代码把每个Contract接口打开看每个方法是不是符合职责边界这套体检做完项目质量大概心里就有数了。