从MVC到MVVM:Android架构演进与项目选型深度拆解
写这篇文章的起因是我最近在某技术社区看到不少人把 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 的基础上换了一个思路用观察者模式实现数据驱动 UIViewModel 暴露可观察的数据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接口把按钮点击事件转发给 Presenterpublic 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 LiveDataJetpack 的 MVVM 实现核心是 ViewModel 和 LiveData。ViewModel 的生命周期跟 Activity 的配置变化解绑了屏幕旋转的时候Activity 销毁重建但 ViewModel 还活着数据不会丢。LiveData 是生命周期感知的观察者只有当界面处于活跃状态时变化才会被推送。登录页面的 MVVM 写法大概是这样的public class LoginViewModel extends ViewModel { private MutableLiveDataBoolean loading new MutableLiveData(); private MutableLiveDataString error new MutableLiveData(); private MutableLiveDataUser user new MutableLiveData(); public LiveDataBoolean getLoading() { return loading; } public LiveDataUser 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 namevm typecom... / /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资源无法复用也不太利于替换测试。第三步建立 ViewModelclass LoginViewModel : ViewModel() { private val repository UserRepository(...) val user: MutableLiveDataUser MutableLiveData() val isLoading: MutableLiveDataBoolean 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 引用而不是具体的 Activitypublic 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(ListData 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(ListData list) { adapter.setData(list); } Override protected void onDestroy() { super.onDestroy(); presenter.detachView(); } }MVVM 页面模板class XxxViewModel : ViewModel() { private val repository XxxRepository() val dataList MutableLiveDataListData() 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 还持有它的数据回调。解决方法统一使用viewModelScopeRepository 不要持有 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()拿不到因为默认的构造器是无参的。解决的办法是写一个 Factoryclass UserViewModel(private val userId: String) : ViewModel() { companion object { fun provideFactory(userId: String): ViewModelProvider.Factory { return object : ViewModelProvider.Factory { override fun T : ViewModel create(modelClass: ClassT): 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 行先别管用的是什么架构大概率已经出问题了。架构是帮你控制失控的而不是让你多写几百行模板代码的。心里常揣着这个红线你选型的时候就不会跑偏。