news 2026/10/6 3:44:35

Android架构演进:三层架构+MVP标准化分层实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android架构演进:三层架构+MVP标准化分层实践指南

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 IBasePresenter<View> { void login(String username, String password); void sendSmsCode(String phone); } }

细粒度接口看起来繁琐,但好处是显而易见的。第一,Presenter不可能随便调View的方法,它只能调用UI展示相关的动作,职责边界在编译期就写死了。第二,写测试的时候,Mock一个View,只要实现这几个方法就行,不用去处理一个万能Object里的复杂分支。第三,后人改代码时,根据接口名字就能知道“登录成功应该走哪个回调”,不至于翻半天代码才能找到展示逻辑在哪。

3.3 泛型BasePresenter和BaseView:把通用逻辑抽干净,但不要抽过头

MVP在项目里落地,基本离不开泛型基类。我常用的基类长这样:

public abstract class BasePresenter<V extends IBaseView> implements IBasePresenter<V> { 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包里几百个Activity,fragment包里上百个Fragment,adapter又跟各个页面业务交织在一起,你根本看不出哪个页面负责什么功能。更麻烦的是,新需求来了,你要在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()这样的调用,必须先找对应的Presenter,Presenter再去找业务层的Interactor,Interactor再依赖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 IBasePresenter<LoginContract.View> { void login(String username, String password); } } // 2. 数据层:仓库接口和实现 public interface UserRepository { Observable<User> login(String username, String password); } public class RemoteUserRepository implements UserRepository { private final ApiService apiService; public RemoteUserRepository(ApiService apiService) { this.apiService = apiService; } @Override public Observable<User> 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 Observable<User> 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 BasePresenter<LoginContract.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 BaseActivity<LoginContract.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(List<Item> 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层的写法和可观测性会更强。但分层的指导思想不会变,数据、业务、表现仍要各管一摊,只不过协作方式从“接口回调”变“数据流驱动”。到时候这套标准化分层设计依然是在底层托底的那张网。个人项目或者团队项目,不妨也按这个路线慢慢演进,每一步都有收获。

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

ibaAnalyzer v7.3.5实战:从RBS/ERDA能谱到深度剖面拟合

简介&#xff1a;ibaAnalyzer_v7.3.5 是一款面向 IT 运维与数据分析场景的专业分析工具&#xff0c;可用于网络流量监测、性能瓶颈定位以及日志数据排查。该版本同时提供 64 位和 32 位安装程序&#xff0c;兼顾现代服务器与老旧系统环境&#xff1b;配套的两份 PDF 分别介绍了…

作者头像 李华
网站建设 2026/10/6 3:44:01

分库分表分片策略选型指南:容量评估、分片键与混合切分实践

这些年做后端开发&#xff0c;凡是聊到数据量增长&#xff0c;几乎必然要碰“分表分库”这个话题。找我咨询的人通常一开口就问&#xff1a;“我们到底用不用做分库分表&#xff1f;分片策略选哪种最好&#xff1f;”但每次我的回答都是&#xff1a;先别急着选策略&#xff0c;…

作者头像 李华
网站建设 2026/10/6 3:43:18

遗传算法、粒子群与差分进化优化K均值聚类:Matlab实战与对比

我在帮客户做一批用户分群的时候&#xff0c;遇到过一件很典型的窝火事&#xff1a;同样是用Matlab里的kmeans跑&#xff0c;第一次迭代7次就收敛&#xff0c;看一眼SSE还挺漂亮&#xff1b;第二次换了个随机种子&#xff0c;结果完全变了样——两个相邻的簇被拆得七零八落&…

作者头像 李华
网站建设 2026/10/6 3:42:22

IAR调试CC1310报错Error -241?XDS110连接失败的完整排查指南

最近拿一块CC1310 LaunchPad调一个低功耗无线透传的例程&#xff0c;IAR里刚点下Download&#xff0c;还没等进度条走起来&#xff0c;就直接弹了一条红色的Fatal error&#xff1a;Failed to connect to the XDS emulator (Error -241 0x0)。这条报错在TI的CC13xx、CC26xx系列…

作者头像 李华
网站建设 2026/10/6 3:42:04

ControlNet云端部署实战:从环境配置到性能优化全指南

先聊几句实在话&#xff1a;ControlNet这名字听起来高大上&#xff0c;但它本质上就是Stable Diffusion的一个方向盘&#xff0c;让你能通过姿态、边缘、深度这些线索精确控制生成图的结构。而“云端部署”这四个字&#xff0c;对很多本地显卡吃紧、或者想把能力开放给团队的人…

作者头像 李华
网站建设 2026/10/6 3:41:35

MySQL XtraBackup 全量备份还原实战指南:从5.7到8.0的完整流程

MySQL的备份还原向来是运维工作里最能折腾人的一块&#xff0c;尤其是数据量上来之后&#xff0c;XtraBackup这种物理备份工具基本成了生产环境的标配。我从 MySQL 5.7 一直用到 8.0&#xff0c;中间用全量备份做过各种还原演练和从库搭建&#xff0c;踩过不少坑&#xff0c;也…

作者头像 李华