写这篇文章的起因是我最近在某技术社区看到不少人把 MVP、MVVM 挂在嘴边,但真要落到代码层面,不少人还是搞不清楚这三兄弟到底有什么区别、各自解决什么问题、项目里到底该用哪个。这个问题我十多年前入行做 Android 时也困惑了很久,后来一直在实际项目里反复迭代,才算是真正摸透了。这篇文章就按我的真实理解,把 MVC、MVP、MVVM 这三套架构从头到尾掰开讲清楚,包括各自的思路、代码长什么样、有哪些坑,以及不同团队怎么选型。
1. 架构模式演进与选型思路拆解
先说个很现实的问题:架构模式本身不产生业务价值,用户不会因为你用了 MVVM 就多付钱。架构的价值在于让代码“可维护、可测试、可协作”。如果一个项目的代码永远是一个人写、不换人、不测试、不扩展,那用什么架构都无所谓。但现实是代码一定会被后来者接手,需求一定会变更,那就必须考虑管理复杂度的问题。
1.1 从 MVC 到 MVVM 的演进逻辑
MVC 是资历最老的架构,它的核心思想是分层:Model 管数据,View 管界面,Controller 管交互逻辑。这个思想本身非常健康,问题出在 Android 平台上。在 Android 里,Activity 和 Fragment 既承担了 View 的职责(界面显示),又天然是事件分发和生命周期管理的入口。于是大家很自然地就把 Controller 的逻辑也塞进了 Activity。结果就是 Activity 越来越臃肿,一个上千行的 Activity 非常常见,里面塞满了业务判断、网络回调、数据解析和界面刷新,测试也无从下手。
MVP 的出现是对这个问题的直接回应。它的核心是把逻辑从 View 里抽出来,由 Presenter 负责处理业务和交互逻辑,View 只做被动的界面展示。这样 View 和 Model 彻底解耦,业务逻辑集中在 Presenter 里,可以独立测试。这是架构演进上非常重要的一步。但我自己也踩过 MVP 的坑:接口数量爆炸。为了做到 View 和 Presenter 互相隔离,每个页面都要定义 View 接口和 Presenter 接口,一个项目十几个页面就是对两倍数的接口,很多细碎的回调让人崩溃。
MVVM 在 MVP 的基础上换了一个思路:用观察者模式实现数据驱动 UI,ViewModel 暴露可观察的数据,View 订阅这些数据的变化,模式不再靠回调而是靠数据绑定。这套思想其实很早就有,但直到 Google 正式推出 Jetpack 里的 ViewModel 和 LiveData,才真正解决了生命周期安全这个最大的隐忧。LiveData 知道界面有没有在前台,只有在活跃状态才推送数据,从机制上避免了 MVP 里常见的空指针和内存泄漏问题。
1.2 三种思维方式的本质区别
我个人理解这三兄弟的本质差异,可以浓缩成一句话:谁控制数据流,以及各方的通信机制。
MVC 下 UI 和逻辑是紧耦合的,用户点击按钮,Controller 直接拿去操作 Model,再让 View 去刷新,控制权在 Activity 这一侧,而 Activity 又和 View 是一体的,所以这套模式的边界在实践中很容易被模糊掉。
MVP 是接口驱动,View 和 Presenter 互相持对方的接口引用,所有交互都是显式的方法调用。用户点了按钮,View 调 Presenter 的方法,Presenter 玩命做逻辑,拿到结果再调用 View 的接口方法刷新界面。这种模式边界很清楚,但代价就是接口多了,而且 Presenter 的生命周期很不安全——它比 View 活得久是好事,但它本身却挂在 View 的销毁时机上,容易泄漏。
2. 核心细节深度拆解:结构与数据流
这一节我们逐个细看三者的源码结构、数据流动方式,以及最关键的生命周期问题。
2.1 MVC 在 Android 中的经典形态
在 Android 里,严格实现的 MVC 其实是缺失的。通常的写法是:
// 经典 MVC 写法:Model 负责数据,Activity 既是 View 又是 Controller public class LoginActivity extends AppCompatActivity { private UserModel userModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); userModel = new UserModel(); findViewById(R.id.btn_login).setOnClickListener(v -> { String username = getEditTextContent(); String password = getEditTextContent2(); // 这里是 Controller 逻辑,直接写在 Activity 里 userModel.login(username, password, new Callback() { @Override public void onSuccess(User user) { // 直接操作 View showToast("登录成功"); startActivity(new Intent(LoginActivity.this, HomeActivity.class)); } @Override public void onError(String msg) { showError(msg); } }); }); } }这种写法不复杂,也不用额外引入任何框架,缺点从代码里就能看出来:Activity 里的逻辑没法单独测试,因为它是直接拿着控件操作数据的。而且 Model 层和 View 层也并没有彻底隔离——如果网络回调里要更新 UI,它直接操作Toast、直接跳转页面,没有中间隔离层。
操作注意:如果项目里已经存在大量这种写法的老代码,想重构的话不要一次性全部推翻,我的建议是“新页面用新模式,老页面坏味道最严重的地方逐个迁移”。断层式重构的阵痛通常大过收益,很容易在业务需求下被迫回滚。
2.2 MVP 契约接口设计的最佳实践
MVP 把接口概念推到了一个新的高度。常规做法是先定一个契约接口(Contract),把所有对外行为都定义在里面:
public interface LoginContract { interface View { void showLoading(); void hideLoading(); void onLoginSuccess(User user); void onLoginError(String message); } interface Presenter { void login(String username, String password); void onDestroy(); } }然后 Presenter 持有 View 的引用,但不持有具体 Activity,而是持有契约接口,这样 mock 起来很容易:
public class LoginPresenter implements LoginContract.Presenter { private LoginContract.View view; private UserModel model; public LoginPresenter(LoginContract.View view) { this.view = view; this.model = new UserModel(); } @Override public void login(String username, String password) { view.showLoading(); model.login(username, password, new Callback() { @Override public void onSuccess(User user) { view.hideLoading(); view.onLoginSuccess(user); } @Override public void onError(String error) { view.hideLoading(); view.onLoginError(error); } }); } }Activity 实现LoginContract.View接口,把按钮点击事件转发给 Presenter:
public class LoginActivity extends AppCompatActivity implements LoginContract.View { private LoginPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); presenter = new LoginPresenter(this); findViewById(R.id.btn_login).setOnClickListener(v -> presenter.login(getUsername(), getPassword())); } @Override public void showLoading() { progressBar.setVisibility(View.VISIBLE); } }核心难点和我的经验:MVP 最大的隐患就是 View 引用的生命周期管理。Presenter 在异步操作回来之前,View 可能已经被用户按返回键销毁了,此时再调用view.onLoginSuccess()就是裸奔的空指针或白屏。还有个情况是网络请求回来时 Activity 已经重新创建了旧实例,这时候回调刷新的界面跟用户当前看到的根本不是同一个。
我当时的解决方案是:给 Presenter 的绑定改成弱引用,在onDestroy里主动调用presenter.onDestroy()解绑。同时对于异步回调,回来后要先判断 view 是否还有效。这些代码写多了你很自然就会想:如果框架层面能自动处理好生命周期就好了,这就是 MVVM 登场的契机。
2.3 MVVM 的 Jetpack 实现:ViewModel + LiveData
Jetpack 的 MVVM 实现,核心是 ViewModel 和 LiveData。ViewModel 的生命周期跟 Activity 的配置变化解绑了:屏幕旋转的时候,Activity 销毁重建,但 ViewModel 还活着,数据不会丢。LiveData 是生命周期感知的观察者,只有当界面处于活跃状态时变化才会被推送。
登录页面的 MVVM 写法大概是这样的:
public class LoginViewModel extends ViewModel { private MutableLiveData<Boolean> loading = new MutableLiveData<>(); private MutableLiveData<String> error = new MutableLiveData<>(); private MutableLiveData<User> user = new MutableLiveData<>(); public LiveData<Boolean> getLoading() { return loading; } public LiveData<User> getUser() { return user; } public void login(String username, String password) { loading.setValue(true); Repository.login(username, password) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { loading.setValue(false); user.setValue(result); }, throwable -> { loading.setValue(false); error.setValue(throwable.getMessage()); }); } }Activity 这边的代码非常薄:
public class LoginActivity extends AppCompatActivity { private LoginViewModel viewModel; private TextView tvUsername; private TextView tvError; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); tvUsername = findViewById(R.id.tv_username); tvError = findViewById(R.id.tv_error); viewModel = new ViewModelProvider(this).get(LoginViewModel.class); viewModel.getUser().observe(this, user -> { startActivity(new Intent(LoginActivity.this, HomeActivity.class)); }); viewModel.getLoading().observe(this, loading -> { progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); }); findViewById(R.id.btn_login).setOnClickListener(v -> viewModel.login(getUsername(), getPassword())); } }这套设计的优雅之处在于:Activity 转屏销毁重建后,新 Activity 会自动拿到同一个 ViewModel 实例,界面状态自动恢复,根本不需要写onSaveInstanceState。
实操心得:ViewModel 里一定不要持有 Activity 或任何 View 的引用。ViewModel 的生命周期比 View 要长,如果持有 View 引用,转屏时旧 View 销毁了但 ViewModel 还挂着,就会产生泄漏。需要Context的时候应该用AndroidViewModel(它持有的是 Application),或者直接在 Repository 层注入。
2.4 MVVM 全家桶的进阶形态
MVVM 在实践中往往不是孤立存在的,而是和 Jetpack 其他组件一起使用。比如配合 DataBinding,布局里可以直接绑定 ViewModel 的数据:
<layout> <data> <variable name="vm" type="com..." /> </data> <LinearLayout> <Button android:onClick="@{() -> vm.login()}" /> <ProgressBar android:visibility="@{vm.loading ? View.VISIBLE : View.GONE}" /> </LinearLayout> </layout>用上 DataBinding 之后,连findViewById都没了,View 层彻底变成一个“壳”,里面只有绑定关系,没有任何逻辑。再配合 Room 做本地存储,配合 Paging 做分页列表,一套完整的数据链路就全部打通了。
坦率讲,DataBinding 不是银弹。我在实际项目里发现它有两个麻烦:一是 XML 里写逻辑导致 UI 层代码不好调试,遇到问题要去翻布局的绑定表达式,排查链路变长;二是数据绑定表达式写不好反而会增加团队阅读成本。所以我现在的风格是:ViewModel + LiveData 是必须的,DataBinding 视团队情况选用,新人多的时候宁可多用几个observe,也不在 XML 里写太复杂的表达式。
3. 实操过程:架构从零到一与重构落地
说空话没有意义,直接看一套完整的落地流程。我们以“开发一个登录 + 列表页面的简单商城”为例子走一遍,从零搭建 MVVM 架构,并且讲一下如果从 MVP 迁过来怎么改。
3.1 零基础搭建 MVVM 架构
先交代项目结构的分层,这个分层决定了所有代码该往哪放。我习惯分三层:
- UI 层:Activity、Fragment、Adapter,只管界面渲染和事件转发
- 业务层:ViewModel,处理页面交互逻辑、拼装数据,当然也承担状态管理的职责
- 数据层:Repository、网络、数据库,负责数据获取与缓存
第一步先把依赖配好:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.x' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.x'第二步建立数据层。写一个UserRepository,里面封装网络请求的方法:
class UserRepository(private val apiService: ApiService) { suspend fun login(username: String, password: String): User { return apiService.login(LoginRequest(username, password)) } }但要注意的是,Repository 最好是全局单例或者通过注入框架管理。如果每个 ViewModel 都自己new一个 Repository,资源无法复用,也不太利于替换测试。
第三步建立 ViewModel:
class LoginViewModel : ViewModel() { private val repository = UserRepository(...) val user: MutableLiveData<User> = MutableLiveData() val isLoading: MutableLiveData<Boolean> = MutableLiveData(false) fun login(username: String, password: String) { isLoading.value = true viewModelScope.launch { try { val result = repository.login(username, password) user.value = result } catch (e: Exception) { error.value = e.message } finally { isLoading.value = false } } } }这里有一个关键点必须强调:viewModelScope.launch里跑的协程是基于 ViewModel 的生命周期的。ViewModel 被清空的时候,协程会自动取消。如果你用全局GlobalScope.launch去发网络请求,回调回来的时候界面早就没了,轻则白屏,重则崩溃。这个点我在 review 代码时发现过好多次,都是新同事图省事直接用了业务框架里不合适的协程写法。
第四步 Activity 里简单订阅:
class LoginActivity : AppCompatActivity() { private val viewModel: LoginViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) viewModel.user.observe(this) { user -> startActivity(HomeActivity.create(this, user)) } viewModel.isLoading.observe(this) { loading -> progressBar.isVisible = loading } btn_login.setOnClickListener { viewModel.login(username_input.text.toString(), password_input.text.toString()) } } }这样一个最简单的 MVVM 页面就搭完了。完整链路是用户点按钮 → View 调 ViewModel 方法 → ViewModel 调 Repository → 数据通过 LiveData 回流 → View 收到变化更新 UI。整个过程中 Activity 既没有业务逻辑,也没有数据加载代码,职责非常干净。
3.2 从 MVP 重构到 MVVM 的迁移步骤
如果你手里已经有一个 MVP 的老项目想往 MVVM 迁移,别企图一次性重写所有页面。我的经验是按下面这套流程来:
第一步:选一个链路最完整且业务不太复杂的页面做试点,比如个人资料页。把原来的 Presenter 里所有逻辑分类:哪些是状态管理,哪些是数据加载,哪些是 UI 操作。状态管理的数据(loading、error、列表数据)全部移到 ViewModel,用 LiveData 封装。
第二步:替换 Activity 中 View 接口的实现。MVP 里View接口有showLoading,hideLoading,onError等方法,这些都是通过方法调用的,迁移到 MVVM 后全部删除,变为对 LiveData 的观察:
viewModel.isLoading.observe(this, ::showLoading) viewModel.error.observe(this, ::showError)第三步:清理接口文件。原来的 Contract 接口里大量方法都专门为 Presenter 准备的,现在 Activity 只观察数据变化,那些接口就全变成死代码,当场删掉。
第四步:把数据获取逻辑从 Presenter 中剥离,下沉到 Repository 层。这一步的意义是让 ViewModel 不要直接接触网络库和数据库。以后换网络库、加缓存,都在 Repository 内部做,View 这一侧完全无感。
3.3 MVP 里最常见且崩溃的问题
有一次我把一个登录页面改成 MVP 架构开发后,联调时发现上了一个 bug:用户点登录,等了大概两秒,网络回调回来时界面已经销毁。我的view接口还是旧 Activity 的实例,结果view.showError()一调用,直接空指针崩溃。
排查这个问题花了不少时间。因为崩溃是偶发的,只在快速退出页面并回来时触发。后来我在 Presenter 里加日志才发现回调还没回来,Activity 已经走到onDestroy。
修复的关键是让 Presenter 持有的是无用的 view 引用,而不是具体的 Activity:
public void attachView(LoginContract.View view) { this.view = new WeakReference<>(view).get(); } public void detachView() { view = null; }然后在网络回调里判断:
if (view == null) return;这种做法在 MVP 阶段属于标准的防御性写法,但每次写都觉得是在打补丁。后来迁移到 MVVM 后,LiveData 自己就感知了 Activity 的生命周期,这套“手动判空”的代码自然就删掉了,整个逻辑清爽很多。
4. 架构选型评估与核心代码模版解析
到了决策的层面,我们聊聊新项目到底该选哪种架构。
我的判断是:如果你对 Android 技术栈比较陌生,团队也没有架构经验,直接用 MVC 先跑通功能,是最安全的。一套过度设计的架构对没有经验的团队是一种负担。
如果团队里对代码分层有一定共识,又特别看重业务逻辑的可测试性,MVP 是很好的历史选择。但现在新起项目我基本不推荐 MVP 了,不是因为 MVP 有问题,而是 Jetpack 已经给了 MVVM 非常完整的体系支撑,自己再用 MVP 就是重复造轮子,代价高。
二三年后的项目,我的建议是无脑 MVVM + Repository 分层,理由很简单:
- ViewModel 解决了配置变更时的数据留存问题
- LiveData 解决了生命周期安全问题
- viewModelScope 解决了异步任务自动取消的问题
- Repository 把数据来源与页面彻底隔离,网络、数据库、缓存随意换
4.1 三个关键决定:谁来做主,数据归谁,状态放哪
架构选型的表面看是模式之争,实际上是要回答三个问题。第一个问题是谁来做主。MVC 是 Activity 说了算,MVP 是 Presenter 说了算,MVVM 是数据说了算。第二个问题是数据归谁管。MVC 里 Action 调的数据散落在各处,MVP 集中在 Presenter 里,MVVM 集中在 ViewModel。第三个问题是状态在哪儿存。MVP 的难点是状态在 Presenter 里,但 Presenter 不持有上下文,存不住;MVVM 的 ViewModel 就是为持久化状态设计的。
团队里只要把这三点讨论清楚,架构本身用哪套反而没那么重要了。很多人纠结用什么模式,其实是没想明白自己的状态到底该怎么管理。
4.2 各架构下的界面代码模板参考
为了减少沟通成本,我把自己常用的骨架代码做成模板。MVC 页面模板:
public class XxxMvcActivity extends AppCompatActivity { private XxxDataManager dataManager; private void loadData() { dataManager.fetch(new Callback() { public void onSuccess(List<Data> list) { runOnUiThread(() -> adapter.setData(list)); } }); } }MVP 页面模板:
public class XxxActivity extends AppCompatActivity implements XxxContract.View { private XxxContract.Presenter presenter; @Override protected void onCreate(...) { presenter = new XxxPresenter(this); presenter.loadData(); } @Override public void onDataLoaded(List<Data> list) { adapter.setData(list); } @Override protected void onDestroy() { super.onDestroy(); presenter.detachView(); } }MVVM 页面模板:
class XxxViewModel : ViewModel() { private val repository = XxxRepository() val dataList = MutableLiveData<List<Data>>() fun loadData() { viewModelScope.launch { dataList.value = repository.fetchData() } } } class XxxActivity : AppCompatActivity() { private val viewModel: XxxViewModel by viewModels() override fun onCreate(...) { viewModel.dataList.observe(this) { list -> adapter.setData(list) } viewModel.loadData() } }三套模板放在一起对比就能看出来,MVVM 页面的代码其实最薄,因为状态管理已经提升到了 ViewModel 层。
4.3 关于 ViewModel 生命周期的一个常见误解
很多人以为 ViewModel 的生命周期“跟 Activity 一样长”,实际上它的作用是跨过 Activity 的销毁重建。但它的终极边界是 Activity 真正 finish。用户彻底关闭了页面,ViewModel 的onCleared()会被调用,里面该做的清理必须在这里做。
我踩过一个很隐蔽的坑:在 ViewModel 里放了一个监听观察系统的某个状态,但忘了在onCleared里面反注册。后果是界面关了,监听还在,数据还在空中传输。后来我写了一个基类 ViewModel,在onCleared里统一释放资源:
override fun onCleared() { super.onCleared() disposable.clear() listener?.stop() }这个细节值得每个开发者重视。ViewModel 是轻量级状态容器,但它的资源持有必须要克制,否则内存的局面会失控。
5. 常见问题与排障实录
最后整理一批我在项目里真实遇到过的问题,也是帮大家提前排雷。
5.1 Activity 销毁后 VM 还在后台更新怎么办
现象:用户退出页面,日志还显示 ViewModel 在刷数据,甚至弹出空对象异常。
排查思路:分两种场景。第一,网络请求没取消。如果是viewModelScope启动的协程,ViewModel 销毁后协程会自动取消,所以不会出现这种情况。如果你用了全局的GlobalScope.launch,那就必须自己取消,这就回到了我们前面强调的不要乱用协程上下文。第二,外部引用泄漏。比如 ViewModel 里的 Repository 把自身注册成了一个全局对象,这时候 VM 即便销毁了,Repository 还持有它的数据回调。
解决方法:统一使用viewModelScope;Repository 不要持有 ViewModel 的引用,它的职责是单纯的去取数据。偶尔有些架构里 Repository 会回调异常导致数据不更新,那种情况大概率是回调线程不对,检查包调用的时候是否切了主线程。
5.2 LiveData 数据倒灌问题
这是 MVVM 里唯一让我最头疼的问题。现象是:页面旋转之后,observe会立刻收到上一次的旧数据,导致界面瞬间闪屏。因为 LiveData 在观察者订阅时会直接将当前数据发出去,如果这个 LiveData 里的数据恰好是最后一条缓存,就会被本地回放。
解决方式有几条路。我一个项目里已经把把SingleLiveEvent和SharedFlow都用过。总的原则:只接收一次的事件流(Toast、跳转、错误提示)不要用普通的 MutableLiveData 来扛,建议用 Kotlin 协程的MutableSharedFlow(replay = 0),它的设计初衷就是处理这类“单发单收”的事件。或者可以自己封装一个只允许一次消费的 LiveData,但那个实现起来州很多,我已经很久没再这么写了。
5.3 ViewModel 与 Activity 之间大量隐性依赖
这是团队合作时最容易冒出来的问题。越用 ViewModel 越容易忽略它跟 Activity 之间的克制。比如有的人会把 Bundle 里的参数直接往 ViewModel 里塞,有的人在 View 层拿数据去判断显示逻辑,结果把 ViewModel 设计成了一个“数据搬运工”,页面逻辑又回到 View 那一层,架构成型等于白做了。
我的检查方法是:看 View 层代码里有多少个if。只要发现 View 代码里出现了条件逻辑分支,比如if (data == null) showEmpty()这种,说明状态管理的职责下放了,该把这块抽象逻辑挪到 ViewModel。View 层的代码只应该有绑定、事件转发、动画效果,全篇不应该有业务判断。
5.4 用 ViewModelProvider.Factory 创建带参数的 VM
很多人在传递参数时遇到困难。比如登录页需要接收 userId 作为初始化参数,直接by viewModels()拿不到,因为默认的构造器是无参的。解决的办法是写一个 Factory:
class UserViewModel(private val userId: String) : ViewModel() { companion object { fun provideFactory(userId: String): ViewModelProvider.Factory { return object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return UserViewModel(userId) as T } } } } }然后 Activity 里这样用:
private val viewModel: UserViewModel by viewModels( extras = { UserViewModel.provideFactory(userId) } )这个方法在面试和实际开发里都要注意,偏好直接把参数作为初始化字段的需求在业务里很常见。
5.5 明确一个原则:什么时候才需要引入架构
架构不是越多越好,而是跟项目需求匹配。比如做一个工具类 Demo,功能只有两个页面,用 MVVM 全家桶反而会在分层和模板代码上浪费大量时间,没有意义。如果一个项目会有长期迭代、多人参与、测试介入,那架构就很有价值。
我的判断标准很简单:项目是否有“未来开发持续 3 个月以上”的预期。如果有,就上 MVVM;如果没有,就用最直接的 MVC 跑通。这不是退步,是务实。
6. 写在最后的卡壳时刻与选择清单
三套架构在我看来从来不冲突,它们的每一次演进都是在回应同一个问题:如何把 Android 开发中天生混杂的“界面”和“逻辑”拆干净。MVC 是提出了分层的意识,MVP 是强调依靠显式接口约束,MVVM 则是用数据驱动的自然机制去替代繁琐的回调。它们的共同优点是每套架构都配了“判决性”的分界,只是程度不同。
对你的现实指导很直接:
- 页面很简单,改动少、生命周期安全要求低 → MVC 没问题,别被架构绑架。
- 页面有复杂的交互逻辑且需要单元测试 → MVP 在逻辑隔离和测试方面有天然优势,但要有抵抗接口泛滥的心理准备。
- 新项目,且配合 Jetpack 生态 → 直接上 MVVM,不需要犹豫。
- 老项目已经积重难返 → 不要在旧页面动手,新页面用新架构,逐步推进改造。
在我这两年带团队的经验里,比选择哪套架构更重要的,是团队里所有人都能一致理解那三个决定:谁做主、数据归谁、状态放哪。只要这三个问题的答案在代码里是被严格遵守的,MVC 也能写出干净的项目。反过来,哪怕用了最标准的 MVVM 全家桶,如果 View 层里到处都是if和业务操作,那也只是形式主义。
最后再分享一个小技巧,我每次评审别人的代码时,第一件事就是看 View 层文件的行数。如果 Activity 超过 500 行,先别管用的是什么架构,大概率已经出问题了。架构是帮你控制失控的,而不是让你多写几百行模板代码的。心里常揣着这个红线,你选型的时候就不会跑偏。