
1. 从“能跑”到“能维护”为什么我盯上了标准化分层入行前几年我对“架构”这件事的态度一直很随意。接到需求就先写页面接口字段还没定义清楚就先把页面布局撑出来业务逻辑能塞进Activity就绝不单独建类工具方法更是随手往一个满目疮痍的Utils.java里堆。项目确实能跑Demo阶段跑得飞快但问题是一旦产品开始提迭代需求噩梦就来了改一个登录流程要翻遍三四个Activity修一个接口返回空值的Bug得从Presenter一路追到Adapter新同事接手熟悉代码光理清AndroidManifest里那二十多个Activity的跳转关系就花了两天。那段时间我复盘过项目之所以乱不是技术不够强而是缺少一个所有人都能遵循的共同约定。代码层面有强类型和编译期检查兜底但结构层面没有。后来我接触了不少成熟项目发现它们都有一个共性——分层清晰职责单一边界明确。不管内部用的是MVP、MVVM还是别的什么模式最底层的东西一定是“分层”。三层的骨架搭稳了MVP只是在这副骨架上决定某一层内部怎么协作的规则。所以这篇文章想聊的不是某个炫酷的框架也不是Kotlin协程或者Jetpack Compose这些新东西而是最朴素、最不容易出错、也最适合中小型团队长期维护的一套组合三层架构 MVP 标准化分层设计。说的直白点就是怎么把项目从“哪里都能写代码”改造成“不该写的代码写不进去”。这套东西在我看来是Android项目从“能跑”走向“能维护”的一道分水岭。适合谁看呢如果你正在带一个三五人的Android小组或者你手头有一个已经有点臃肿、但还不至于推倒重来的老项目再或者你正在从“一个人写所有代码”过渡到“团队协作开发”的阶段这篇内容应该对你有用。里面涉及的分层思路、包结构规范、MVP拆分技巧都是我踩过坑之后沉淀下来的实践方案可以直接照着抄也可以根据你的项目规模裁剪。2. 三层架构到底在分什么数据、业务、表现各管一摊很多刚接触分层的人会把“三层架构”和MVP、MVVM混为一谈其实它们是两个维度的问题。MVP解决的是“在一个模块内部界面逻辑和业务逻辑怎么解耦”而三层架构解决的是“整个App的数据流怎么组织各层之间的依赖方向往哪走”。你可以把三层架构看作整栋楼的承重墙MVP只是某一层内部装修时选的家具风格。承重墙歪了家具再好看也是白搭。2.1 数据层离数据最近的一方只回答“从哪拿存到哪”数据层传统Android项目里经常被叫做model但很多人的model包里塞的全是JavaBean这其实是对数据层的误解。数据层不是只放实体类它应该负责所有跟数据打交道的工作网络请求的发起和响应解析、本地数据库的读写、SharedPreferences的存取、文件缓存以及把这些数据源的细节向上层屏蔽。我见过一个很大的误区很多人把Retrofit的ApiService接口直接塞给Presenter用Presenter里同步写网络回调Activity里再根据回调结果切UI状态。这样看起来也没毛病但问题在于当接口从Retrofit换成OkHttp或者本地加了一套Room缓存所有用到了这个接口的地方都得跟着改。数据层的意义就在于让上层只知道“有一个UserRepository.loadUser()方法能拿到用户信息”至于这个信息是从服务器拉来的、还是从缓存读出来的上层一概不关心。所以数据层的标准做法是定义一组对外暴露的Repository接口内部再通过DataSource的细分实现来分别处理远程数据、本地缓存和内存缓存。实体类也可以放在这一层但要意识到实体类只是数据的载体不是这一层的核心逻辑所在。2.2 业务层承接“用户想干的事”把零散数据编排成完整动作业务层在三层架构里位置介于数据层和表现层之间很多MVP项目里这一层的职责被Presenter顺带承担了。但这恰恰是我觉得最值得讨论的地方Presenter到底算不算业务层在我早期写的MVP代码里Presenter里经常出现这样的逻辑if (user ! null user.getVipLevel() 2) { // 计算折扣 double price product.getPrice() * 0.8; // 调支付接口 api.pay(order, price, callback); // 回调里更新UI view.showPaymentSuccess(result); }这段代码其实混了三件事判断VIP折扣是一段纯业务规则调支付接口是一次数据操作更新UI是表现层动作。把它们全塞在Presenter里工作能完成但代码会随着业务复杂度上升迅速膨胀。比如“支付”可能还要校验库存、校验优惠券有效期、记录风控日志这些逻辑如果都在Presenter里写到最后Presenter动不动就上千行。因此我更倾向于把业务层单独拆成一层用**Interactor或者叫UseCase也有人叫Domain层**来承载一次完整业务的编排。Presenter只负责两件事从View拿到用户意图以及把业务层的结果翻译成View可以展示的状态。至于支付流程里的折扣计算、库存校验、日志上报都是Interactor内部的事。数据层给Interactor提供原子的数据能力Interactor把这些原子能力按业务规则组合成一次有意义的动作再把结果抛给Presenter。2.3 表现层只关心“界面长什么样”不关心“数据怎么来的”表现层就是Activity、Fragment、View以及它们对应的契约接口。这一层应该是最“傻”的一层用户点了登录按钮表现层就把用户名密码打包成参数交给Presenter去执行然后等着Presenter回调loginSuccess(String token)或者loginFailed(int code, String msg)。至于这个Token是怎么从服务器换来的接口是不是要拼接签名登录失败是网络超时还是密码错误表现层一概不知。标准的层间依赖关系是这样的表现层依赖业务层业务层依赖数据层数据层不依赖任何上层。Android里最常见的问题就是表现层直接依赖数据层——Activity里直接new一个Retrofit实例或者把SQLiteOpenHelper传给Adapter。这种写法一旦引入MVP就会让契约接口名存实亡。所以我在改造项目时会强制规定表现层代码里不允许出现OkHttp、Retrofit、SharedPreferences这些单词。如果哪个文件里出现了就是分层违规必须改。3. MVP在三层架构里扮演的角色契约、梳理与反向控制讲完成三层再看MVP就顺多了。MVP的核心不是“把Click事件抽到一个类里”而是定义一套表现层内部的交互契约。在三层架构的掩护下MVP真正要做的事是把“表现层”这个抽象概念落实成三个具体角色View、Presenter、Contract。3.1 Model是“外包”给数据层和业务层的MVP里其实没有Model这是个很有意思的细节。经典MVP的定义是Model-View-Presenter其中Model负责数据和业务规则。但我在三层架构下实践下来发现如果你已经认认真真拆了业务层和数据层那MVP里的Model就可以直接理解成这两层的对外能力压根不需要再单独建一个model包。我曾经看到有人的MVP项目里既有bean包、api包又在MVP里搞一个model包放一堆LoginModel、UserModel里面全是重复的代码纯粹是为了凑“M”这个字。咱们务实一点MVP里的M就是一个抽象概念它代表的是Presenter需要的数据和业务能力。在三层架构里这个能力来自Interactor和Repository。所以我在实际项目里一个MVP模块的代码根本不会有model目录取而代之的是data和business这样的顶层分包。结构变得清爽不说还杜绝了那种“Model里塞业务、业务里写网络请求”的混乱局面。3.2 View和Presenter的契约宁可多写几个接口也不要让View被摸透MVP里另一个容易走偏的地方是View接口写得过于粗糙。很多人定义View接口时经常就是长这这样public interface LoginView { void showResult(Object result); }一个showResult(Object)完事什么登录成功、登录失败、用户名错误、服务器异常全用Object塞。这样的接口等于没有接口Presenter想做什么都得靠if判断类型View也要写一堆switch来拆分结果。到最后View接口里全是void showXxx(Object data)这样的万能方法MVP的解耦效果打了对折。正常做法应该根据业务场景把View接口细粒度化public interface LoginContract { interface View extends IBaseView { void showLoading(); void hideLoading(); void onLoginSuccess(LoginData data); void onLoginFailed(int errorCode, String message); void onUserNameError(String message); } interface Presenter extends IBasePresenterView { void login(String username, String password); void sendSmsCode(String phone); } }细粒度接口看起来繁琐但好处是显而易见的。第一Presenter不可能随便调View的方法它只能调用UI展示相关的动作职责边界在编译期就写死了。第二写测试的时候Mock一个View只要实现这几个方法就行不用去处理一个万能Object里的复杂分支。第三后人改代码时根据接口名字就能知道“登录成功应该走哪个回调”不至于翻半天代码才能找到展示逻辑在哪。3.3 泛型BasePresenter和BaseView把通用逻辑抽干净但不要抽过头MVP在项目里落地基本离不开泛型基类。我常用的基类长这样public abstract class BasePresenterV extends IBaseView implements IBasePresenterV { private V view; protected CompositeDisposable disposables new CompositeDisposable(); Override public void attachView(V view) { this.view view; } Override public void detachView() { if (view ! null) { view null; } disposables.clear(); } Override public V getView() { return view; } protected void addDisposable(Disposable disposable) { disposables.add(disposable); } }这里有个很重要的细节——attachView和detachView。为什么要在detachView里清空CompositeDisposable因为Android的组件有生命周期比如网络请求还没回来Activity已经被用户按返回键销毁了。如果不解除View引用请求回来时Presenter还握着这个已经销毁的View轻则空指针重则内存泄漏。所以我强制要求所有异步回调走到Presenter之前都要过一遍view null检查。基类抽通用逻辑是好事但千万不要把业务逻辑也塞进基类里。我见过有的项目BasePresenter里直接写了parseResult()、showErrorToast()这些方法逼着所有子类都继承这些行为。一旦某个页面的错误提示风格不一样就得在子类里覆写甚至改基类。这是典型的过度抽象。基类只放那些所有Presenter都一致的逻辑比如View绑定、异步清理、生命周期事件分发。凡是某个业务特有的一律不放。4. 标准化分层设计一套能落地的包命名与依赖规则理论聊完真正到了动手改项目的时候最实用的其实是“标准和约定”。团队协作讲究的是规则先行三五个人的小组如果每个人都有自己的包结构癖好那项目迟早变成巴别塔。下面我会详细拆解我落地时用的分包方案和依赖禁止规则这些都是可以直接复制到项目里照着做的。4.1 顶层包结构怎么分不要按“类型”分要按“层功能”分很多新手项目喜欢这样分包com.xxx.myapp/ ├── activity/ ├── fragment/ ├── adapter/ ├── model/ ├── api/ └── utils/这种分包方式用一句话评价就是“按文件夹分类不是按架构分层”。activity包里几百个Activityfragment包里上百个Fragmentadapter又跟各个页面业务交织在一起你根本看不出哪个页面负责什么功能。更麻烦的是新需求来了你要在activity、fragment、adapter、model、api五个包里来回跳才能凑出一个完整功能的上下文。我改造后的分包思路是以“功能模块 层级角色”来组织代码。典型的顶层结构如下com.xxx.myapp/ ├── app/ │ ├── App.java │ ├── di/ // 依赖注入配置 │ └── utils/ // 全局工具尽量少 ├── data/ │ ├── local/ // 数据库、Preference、文件 │ ├── remote/ // 网络请求 │ ├── repository/ // 对外暴露的仓库接口与实现 │ └── entity/ // 实体类 ├── business/ // 业务层 │ ├── login/ // 登录模块的Interactor │ ├── order/ // 订单模块的Interactor │ └── common/ // 通用业务 ├── ui/ │ ├── login/ // 登录页面相关 │ │ ├── LoginActivity.java │ │ ├── LoginContract.java │ │ ├── LoginPresenter.java │ │ └── LoginAdapter.java │ ├── main/ │ ├── order/ │ └── common/ ├── widget/ // 自定义控件 └── di/ // 全局依赖注入这种结构最大的区别在于一个功能的所有表现层代码聚在一起。你打开login包页面、契约、Presenter、列表适配器一目了然业务层有独立的login包对应数据层有repository提供接口。改一个需求不再需要跨无数包找文件基本就是上下游各一层动两个文件的事。4.2 依赖方向的红线谁也不能越级调用分完包以后约束依赖方向是分层设计的第二步。没有依赖约束的分层等于没分。我在项目里定了几条红线并且通过Code Review和定时脚本检查来强制执行。第一ui层不允许直接调用data层的方法。比如说Activity里绝对不能出现UserRepository.instance().getUserInfo()这样的调用必须先找对应的PresenterPresenter再去找业务层的InteractorInteractor再依赖Repository。为什么要这样绕一层因为UI层直接调数据层意味着UI和具体的数据获取方式耦合了日后加缓存、加失败重试甚至更换数据源都会导致UI代码改动。Presenter或者Interactor存在就是为了在这些变化之间提供缓冲垫。第二business层不允许依赖任何ui层里的类。Interactor里不能出现Activity、Fragment、Adapter的引用。这项规则能保证业务规则是跟界面形态无关的那套登录的业务逻辑拿到手机上能用以后做成车载机、电视盒子逻辑代码可以直接复用。第三data层不能依赖business和ui层。数据层是最底层的相当于地基地基不能去引用楼上的东西。这条规则更多是靠包结构自然封住只要data包里的文件不去import上层包就OK。为了杜绝手滑我建议用Gradle的checkstyle或者AndroidLint来配置依赖规则把越级调用直接作为编译错误拦截掉。这个后面会具体讲怎么配。4.3 标准MVP模块的代码样板从Activity到Repository一张图说清理论规则定完拿一个最常见的登录接口来举例把标准MVP模块的代码长什么样完整演示一遍。假设这个Login页面至少涉及用户名密码输入、登录按钮、加载状态、成功跳转、失败提示。几个需要关注的关键点第一Activity作为View层绝对不能持有缓存、网络、数据库的任何引用。它只负责把输入数据传给Presenter以及根据Presenter的回调去更新界面控件。这里有一个实践细节点击登录按钮后Presenter内部要保证在请求期间对View的调用是有节制的不要疯狂回调所以一般会在请求开始时就view.showLoading()然后业务层的结果回来以后再做下一次回调。第二Presenter中要同时用到业务层和数据层。为了不让Presenter直接依赖具体的网络框架和持久化实现它依赖的是LoginInteractor接口和UserRepository接口这些接口实例通过构造方法或注入框架传入。最简单的做法在App类里维护一个全局的RepositoryProvider负责统一创建单例Repo和Interactor。项目规模上来以后再引入Dagger或者Hilt也不迟。第三业务层和数据层的封装可以做到完全不知道上层是谁。LoginInteractor只是做了“从Repository拿用户数据、校验密码、判断登录状态”这三件事它不关心结果是被Activity展示还是被一个测试用例断言。下面我把这个样例组合成一份可运行的代码骨架你照着写一遍就能体会这条调用链到底爽在哪里。// 1. 契约接口规定View和Presenter互调的方法 public interface LoginContract { interface View extends IBaseView { void onLoginStart(); void onLoginSuccess(User user); void onLoginFailed(String msg); } interface Presenter extends IBasePresenterLoginContract.View { void login(String username, String password); } } // 2. 数据层仓库接口和实现 public interface UserRepository { ObservableUser login(String username, String password); } public class RemoteUserRepository implements UserRepository { private final ApiService apiService; public RemoteUserRepository(ApiService apiService) { this.apiService apiService; } Override public ObservableUser login(String username, String password) { return apiService.login(username, password) .map(LoginResponse::getUser); } } // 3. 业务层编排一次登录操作 public class LoginInteractor { private final UserRepository userRepository; public LoginInteractor(UserRepository userRepository) { this.userRepository userRepository; } public ObservableUser login(String username, String password) { // 这里还可以做格式校验、记录日志、尝试缓存等 if (username null || username.trim().isEmpty()) { return Observable.error(new ApiException(10001, 用户名不能为空)); } return userRepository.login(username, password); } } // 4. 表现层Presenter public class LoginPresenter extends BasePresenterLoginContract.View implements LoginContract.Presenter { private final LoginInteractor loginInteractor; public LoginPresenter(LoginInteractor loginInteractor) { this.loginInteractor loginInteractor; } Override public void login(String username, String password) { if (getView() ! null) { getView().onLoginStart(); } addDisposable(loginInteractor.login(username, password) .subscribe(user - { if (getView() ! null) { getView().onLoginSuccess(user); } }, throwable - { if (getView() ! null) { getView().onLoginFailed(throwable.getMessage()); } })); } } // 5. 表现层Activity public class LoginActivity extends BaseActivityLoginContract.Presenter implements LoginContract.View { private Button btnLogin; private ProgressBar progressBar; Override protected void initViews() { btnLogin.setOnClickListener(v - { presenter.login(etUsername.getText().toString(), etPassword.getText().toString()); }); } Override public void onLoginStart() { progressBar.setVisibility(View.VISIBLE); } Override public void onLoginSuccess(User user) { progressBar.setVisibility(View.GONE); startActivity(new Intent(this, MainActivity.class)); } Override public void onLoginFailed(String msg) { progressBar.setVisibility(View.GONE); Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); } }看到没有整个链路里LoginActivity只认presenter.login()和onLoginStart/onLoginSuccess/onLoginFailed它不知道网络与缓存存在LoginPresenter只依赖LoginInteractor它不知道数据是来自服务器还是数据库LoginInteractor只依赖UserRepository它也不关心View长什么样。这就是一套典型的“从右往左谁都不认识谁”的干净分层。5. 实战改造从乱成一锅粥到井井有条的四步走理论再好如果不实操那就是空谈。接下来我分享一次真实项目的改造流程。这个项目原本是一个标准的“坨坨”结构六七个ArtifactFragment几十个Adapter网络请求直接写在Activity里。目标是在不影响现有功能的前提下逐步往标准分层切换。我总结为四步走每一步都有明确的产出和验收标准。5.1 第一步梳理模块边界确立MVP模块清单改造第一个月我做得最多的事不是写代码而是开会和画图。把产品现有的页面清单列出来按照功能域归成模块。比如登录注册、首页、订单、个人中心、设置。然后给每个模块划定三层的范围哪些UI类属于这个模块哪些业务逻辑属于这个模块哪些数据接口是这个模块需要的。这一步产出物是一张模块-层级的对应表后面所有代码调整都以这张表为准。阶段验收标准每一个现有页面都能在表上找到它的归宿不存在“这个F给它单独一个包”的孤儿页面。5.2 第二步先抽数据层再做业务层最后改表现层改代码的阶段我遵循“从下往上改”的顺序。因为数据层是地基地基稳了上面的改动才不容易推翻重来。先把所有网络请求收拢到一个RemoteDataSource里把所有本地存取收拢到LocalDataSource再定义好Repository接口用实现类去组合这些DataSource。这一步不涉及业务逻辑重组纯机械重构回归测试压力小。接着新增business包把明显属于业务编排的代码从Presenter里往外搬。这里有一个小技巧先不要着急一步到位而是先把纯数据获取的部分下沉到Interactor把UI刷新部分留在Presenter等到Presenter瘦身到一个合理的体量比如不超过200行再做更细的业务拆解。表现层改动最晚。因为只有业务层和数据层稳定下来了才谈得上去改Activity、契约接口和Presenter的引用。在这个阶段我会全线铺开MVP把Activity中所有数据相关的逻辑替换成对Presenter的调用。这也是最容易出Bug的阶段所以强烈建议每个页面改完立刻跑一遍回归别攒着一起改。5.3 第三步统一命名规范消灭“又臭又长”的实现细节分层只解决了“代码放哪里”没解决“代码怎么写”的问题。真正想要标准化还得靠命名规范。我这里列几条在项目里实际执行的效果比较好的规则第一契约接口统一叫XxxContract内部放View和Presenter两个子接口。第二Presenter类统一叫XxxPresenter实现XxxContract.Presenter。View层统一叫XxxActivity或XxxFragment实现XxxContract.View。第三数据层对外接口统一叫XxxRepository实现类如果是远程的就叫RemoteXxxRepository如果是本地就叫LocalXxxRepository。如果某个业务要同时读写多数据源那就再包一层比如UserRepositoryImpl。第四业务层的Interactor命名按动词短语来比如LoginInteractor、FetchOrderListInteractor、SubmitOrderInteractor。避免使用UserManager、HttpHelper这种含义模糊的名字。命名规范看似不起眼但团队协作时它的作用比想象中大得多。新同事拿到需求看到OrderContract自然就知道去OrderPresenter里看逻辑去OrderRepository里找接口。这比读多少文档都省力。5.4 第四步用工程手段固化分层规则靠人不如靠规则分层规范靠口头约定时间久了必然被打破。所以纯靠自觉不算数还需要工程手段强制。推荐三个工具第一个是Checkstyle。配置一些简单的规则比如禁止ui包下的类引用data包下的类。做法是在Checkstyle的customImportOrder或者IllegalImport里写上不允许的包前缀构建时自动校验违反就编译失败。我常用的检查规则之一是禁止所有Activity和Fragment中importretrofit2、okhttp3、android.database.sqlite这些类。第二个是AndroidLint。用它检测内存泄漏、WeakReference误用等跟MVP生命周期有关的问题。尤其是View泄漏Lint能抓出一部分。第三个是依赖注入框架。如果项目有条件引入Hilt或Dagger依赖关系在编译期就能得到更严格的约束。但我们小团队一开始引入Dagger成本偏高我是先用手写的RepositoryProvider做了大半年等分层稳定了才迁移的。没有规则的项目越到后期越不敢动代码有规则保障的项目即使改错了红线也能第一时间发现问题避免上线后炸雷。6. 常见坑与排查实录这五个问题我几乎每个项目里都碰到过在推行标准化分层过程中我光自己就踩了不下十个坑团队的同事更是花样百出。这里挑最典型的五个记录一下后面的人如果再碰到至少有个排查思路。6.1 坑一View接口回调太多走查代码变成“找针”有段时间我把View接口拆得太细导致一个页面十来种回调方法比如showLoading()、showEmpty()、showNetError()、showListData()、showListFooter什么的。结果就是Presenter里到处判断条件去调不同方法View里的方法名多到记不住写起来反而更累。排查之后发现很多回调其实可以统一成三种状态加载状态、数据状态、错误状态。所以后来我把View接口收敛成三个基础方法void showLoading(boolean isLoading); void showContent(ListItem items); void showError(int code, String msg);细分方法只在一块界面上需要特殊展示时才会补充。这个度要自己把握原则是“回调方法数量跟界面状态复杂度匹配而不是跟接口返回值数量匹配”。6.2 坑二Presenter持有View静态引用导致Activity泄漏刚用MVP的时候有人觉得Presenter挂到Application级别更方便就做了一个静态的presenter仓库Activity销毁以后Presenter还持有View引用。结果内存泄漏界面转向都卡。后来我把View引用定义成WeakReference并且强制在detachView()里清空。这么做以后内存稳定不少。补充一点detachView()和RxJava的dispose()必须成对出现。如果只调detach不清理订阅网络回来以后回调还是会跑只是view判空保护了崩溃但订阅仍然存在积累多了同样不好。所以在BasePresenter里把这两件事一并处理。6.3 坑三业务层、数据层、表现层概念混淆导致“伪分层”最典型的现象是代码里明明有LoginPresenter、LoginActivity、LoginRepository但Repository里却写着一大段业务判断。比如检查用户是否VIP、判断优惠券是否过期这些都是业务规则因为写得顺手被塞进了UserRepository里。结果业务一变还得去动数据层数据层因此不稳定。这类问题单纯靠代码检查看不出来需要拉出文件然后逐行问一句“这个逻辑属于谁”它操作的是“数据怎么存”还是“业务怎么算”是前者就放到Repository是后者就得搬进Interactor。这种意识需要刻意培养我带团队时会每周挑几个文件的代码做小Review专门检查职责归属持续两三个月基本就养成了。6.4 坑四过度设计一上来就整完整的分层、MVVM、Dagger全家桶还有一类反面案例是改造时步子迈得太大项目规模明明只有十几个页面却硬塞了仓库、用例、接口、DTO、VO、ViewModel、LiveData、协程外加上一套复杂的Dagger依赖图。结果就是代码量翻倍运行性能下降团队成员驾驭不了最后回退。我的建议是分层标准不必一开始就追求完备可以先从“表现层和业务层分离”“业务层和数据层分离”这两条核心规则做起。当项目确实出现数据源切换的需求、或者业务复杂度明显膨胀时再补上Repository接口和Interactor层。分层的最终目的是降低维护成本如果分层本身变成一种维护负担那就本末倒置了。6.5 坑五测试无从下手MVP也没带来可测性MVP的初衷之一是方便单元测试但很多人的MVP项目写起来照样没法测原因就在于Presenter里藏了大量Android相关的东西。比如直接调了Toast、getResources()、Intent。这些是View要做的事不是Presenter该碰的。Presenter只应该返回结果或状态。为了让Presenter可测我有两条铁律第一Presenter里不允许出现Activity、Context、View的直接引用。所有要展示的文案、颜色、跳转参数一律通过View接口方法传下去比如view.showErrorTip(用户名不能为空)。第二异步调用统统通过接口注入测试时传入假的Interactor直接返回mock数据。能做到这两点Presenter就能在纯JVM环境里跑单元测试。我在改造完登录模块后用Mockito写了一个Case断言用户名为空时Presenter回调onLoginFailed并携带错误码整个测试跑完不用起模拟器几十毫秒出结果这种感觉之前是不敢想的。7. 标准化分层的额外收益不止是代码变整齐还有团队效率的提升文章写到这里有人可能会说“我就是个小项目就我一个人写也要整这么复杂吗”我的看法是如果你是纯个人项目且代码量不超过两三个屏幕页面这套东西确实有点重。但只要你开始跟别人协作或者项目有半年以上的生命周期标准化分层的收益就会迅速凸显。最直接的收益是新成员快速上手。团队里来一个新人不用再让老员工花一周时间口语介绍项目结构直接给他看包结构图和命名规范再去读两个MVP模块样例基本就能上手写代码了。因为写代码的位置是确定的出了问题找代码的位置也是确定的沟通成本大幅下降。第二个收益是重构和迁移成本低。比如项目把图片加载从Glide换成Coil或者把网络框架从OkHttp换成Ktor只要数据层封装得好上层代码可能一改都不用动。之前我在一个老项目里要做数据层迁移原本预期至少要两周因为分层后有Repository这个隔离带结果只改了一个数据层模块和少量配置三天就搞完了个别适配点也很快定位。第三个收益跟“责任”有关。一个人写代码的时候写乱了是个人问题团队写代码的时候代码质量就是组织问题。分层标准提供了一套客观的、可审查的、可激励的代码评价维度。“谁的代码越过了层依赖红线”比“谁的风格不好看”要容易界定得多。这也是为什么我坚持用自动化构建工具来卡分层规则而不是靠人品靠Review。8. 个人心得与后续可以继续做的事讲完方法论和踩坑实录最后分享几点个人感受。第一标准化分层一定是循序渐进的。不要指望一蹴而就把旧项目一次性改完不现实。我从接手那个乱项目到基本完成标准化花了差不多三个多月而且是边做业务边重构。每周抽两天时间专门处理分割出来的小模块才逐渐把整个项目的依赖理顺。如果你现在也面对一个“老烂项目”别焦虑把范围控制在当前正在迭代的功能上一点一点往标准结构上靠长期积累下来变化就很可观。第二MVP和三层架构不是银弹。它解决的是结构问题但解决不了性能问题也解决不了业务复杂度本身。业务如果设计得一团糟再漂亮的分层也只是让垃圾分门别类地摆放。所以我更建议你把分层看作“医疗上的维生素”而不是“救命的药”。在此基础上持续重构、持续校验、持续整理才是项目维护的正道。第三后续我想在这个基础上再往前推一步把表现层从MVP升级到MVVM用ViewModel LiveData/StateFlow来替代手写Presenter的部分工作。因为Android官方对生命周期感知的组件支持越来越成熟View层的写法和可观测性会更强。但分层的指导思想不会变数据、业务、表现仍要各管一摊只不过协作方式从“接口回调”变“数据流驱动”。到时候这套标准化分层设计依然是在底层托底的那张网。个人项目或者团队项目不妨也按这个路线慢慢演进每一步都有收获。