移动应用架构设计实战:MVVM的那些事儿
开篇:为什么选择谈这个话题?

去年,我参与了一个中型电商类App的重构项目。当时团队已经意识到原有的MVC架构在代码量膨胀之后带来的种种问题——Activity臃肿、逻辑混乱、维护困难。尤其是每次产品需求变更时,我们总是被“改一个功能牵一发动全身”的魔咒折磨。
于是我们决定尝试用MVVM(Model-View-ViewModel)架构模式对项目进行模块化重构。这不是一个简单的技术选型,而是一次彻底的开发流程和协作方式的转变。
这篇文章我想通过亲身经历的项目案例,谈谈我们是如何一步步实践MVVM架构、遇到哪些坑,又是如何解决的。希望能给正在做架构选型或者准备重构的你一些启发。
项目背景:一次痛苦的迭代经历

这个电商App最初是典型的MVP架构,但随着业务复杂度的提升,Presenter层变得越来越重。尤其是在用户中心、订单管理这些核心页面中,一个Presenter文件经常超过2000行,修改起来如同走钢丝。
更糟的是,很多公共逻辑被硬编码进Activity/Fragment中,导致重复代码泛滥。比如商品详情页的“加入购物车”按钮点击事件,在三个不同页面上各有一份几乎一模一样的实现。当产品突然要求所有加购按钮都加上埋点统计的时候,我和同事一边骂娘一边手动修改每个地方……
这时候我们意识到必须做出改变了。在技术选型会议上,有几个方案摆在面前:
- 继续优化MVP
- 尝试MVVM + LiveData
- 接入状态管理框架(如Redux)
- 使用Jetpack Compose直接构建新界面(但我们还不敢)
考虑到现有项目的可迁徙性、学习成本以及未来可维护性,我们最终选择了基于Jetpack组件的MVVM架构,配合Data Binding和ViewModel+LiveData作为核心方案。
遇到的挑战:不只是技术上的事

开始转型之后,最大的挑战其实不是代码结构怎么拆分,而是整个团队协作方式的变化。
挑战1:代码责任划分不清晰
刚开始大家对ViewModel到底应该包含什么内容理解不一致。有的同学认为ViewModel就是把原来的Presenter换了个名字,结果ViewModel里照样写了网络请求、数据处理、甚至UI操作相关的代码。
这完全违背了我们使用MVVM的初衷 —— 要让UI与逻辑解耦。
挑战2:生命周期带来的副作用
我们在ViewModel中封装了一些异步任务,但没有做好异常处理机制。某个页面因为ViewModel未正确释放,导致异步回调中的UI更新崩溃。
这种错误一开始频发,后来我们不得不制定统一的协程Scope管理和异常捕获规范。
挑战3:Data Binding带来的学习曲线
刚引入Data Binding时,大家都觉得“写XML比以前多了好多@{xxx}”,抱怨不断。有些经验丰富的开发者甚至坚持要在代码中绑定监听器而非使用BindingAdapter。
这其实是一个习惯问题,我们花了两三个月才真正体会到双向绑定带来的便利。
解决思路与方案设计
我们围绕Google官方推荐的Android Jetpack组件来搭建整体架构:
ViewModel + LiveData + Repository + Room + Retrofit
整体架构如下:
Repository
↑
ViewModel
↑
View(Activity / Fragment / Compose)
架构职责分工明确
| 层级 | 职责 |
|---|---|
| View | 响应用户交互,观察ViewModel变化并更新UI |
| ViewModel | 管理当前页面UI相关的状态和业务逻辑 |
| Repository | 数据仓库,负责数据来源(本地/网络)切换,提供统一接口 |
| DataSource | 数据源接口,定义网络请求或数据库访问逻辑 |
这样的分层结构让我们在后续开发中更容易控制代码质量。
实践中的关键代码示例
接下来我分享几个核心模块的代码片段,供参考:
ViewModel的初始化和数据加载
class ProductDetailViewModel(
private val repository: ProductRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val productId = savedStateHandle.get<String>("product_id") ?: ""
private val _productLiveData = MutableLiveData<Product>()
val productLiveData: LiveData<Product> = _productLiveData
fun loadProductDetail() {
viewModelScope.launch {
try {
val result = repository.getProductDetail(productId)
_productLiveData.postValue(result)
} catch (e: Exception) {
// 异常通知
eventBus.sendEvent(ShowToastEvent("加载失败"))
}
}
}
}
小提示:
SavedStateHandle用于保存实例状态,非常适合用在需要恢复的状态场景中。
Repository层整合网络与本地存储
class ProductRepository(private val apiService: ApiService, private val db: AppDatabase) {
suspend fun getProductDetail(id: String): Product {
val fromDb = db.productDao().getById(id)
if (fromDb != null && !isExpired(fromDb)) {
return fromDb
}
val response = apiService.getProductDetail(id)
val product = response.data
db.productDao().insert(product)
return product
}
}
这样设计可以很容易地扩展缓存策略或引入其他数据源。
Data Binding简单示例
在布局XML中:
<layout>
<data>
<variable name="viewModel" type="com.example.ProductDetailViewModel"/>
</data>
<TextView
android:text="@{viewModel.productLiveData.title}"
... />
</layout>
在Fragment中绑定:
val binding = FragmentProductDetailBinding.inflate(inflater)
binding.viewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
别小看这几行代码,它能有效减少一大半的手动findViewById调用。
踩过的坑和解决办法
说了优点不能不说痛点。下面是我整理的一些真实踩过的坑,希望你不再重蹈覆辙。
坑1:ViewModel内存泄露
我们在初期犯过最严重的错误就是在ViewModel中持有Activity引用。例如:
class MyViewModel(context: Context) : ViewModel() { ... }
这样做会导致ViewModel无法回收,从而引发内存泄露。正确的做法是绝不传入Context对象,如果确实需要上下文资源,可以通过Application提供的上下文。
修复方法:
class MyViewModel(application: Application) : AndroidViewModel(application) {
...
val context = getApplication<Application>().applicationContext
}
坑2:LiveData没有防抖机制
有一个页面会频繁触发筛选条件变动事件,每次改动都会发送搜索请求。但我们发现短时间内多次点击筛选项,会导致多个相同的请求被打到后端。
解决方案很简单 —— 我们用 DistinctUntilChanged() 来去重:
viewModel.searchQuery.observe(viewLifecycleOwner) { query ->
if (query != lastQuery) {
// 发送新请求
}
}
或者在ViewModel内部就做节流处理:
private val _searchInput = MutableLiveData<String>()
val searchResult: LiveData<Result> = _searchInput.switchMap { input ->
liveData {
delay(300) // 输入防抖
emit(repository.search(input))
}
}
坑3:Data Binding性能问题
Data Binding默认开启的表达式绑定在大型项目中可能会带来一定的性能损耗,特别是在RecyclerView中频繁更新时会出现卡顿现象。
优化建议:
- 合理使用
<layout>层级,避免过度嵌套。 - 对于动态列表,可以在适配器中显式设置Binding变量而不是依赖自动绑定。
- 打开DataBinding的增量编译特性:
android {
buildFeatures {
dataBinding = true
}
}
改造后的效果和收益总结
经过为期半年的逐步迁移和重构,我们的项目结构变得更加清晰,主要体现在以下几个方面:
✅ 可维护性提高
- ViewModel和Repository的职责分明,业务逻辑集中在单一位置,查找、调试更方便。
- 修改UI不影响业务代码,反之亦然。
- 多人协作时冲突变少。
✅ 测试更友好
- ViewModel无依赖,可以直接单元测试。
- Repository也可mock数据进行快速验证。
- UI测试也更聚焦于实际交互。
✅ 性能优化空间更大
- 数据驱动UI的方式使得局部刷新更容易。
- ViewModel的生命周期与系统生命周期解耦,有利于资源回收。
当然,重构也不是灵丹妙药。我们在初期还是付出了一定的“转型成本”,包括编写文档、组织培训、Code Review标准重建等。
我的经验分享给正在看这篇文章的你
🧱 架构没有银弹,适合自己的才是好架构
MVVM不是一个万能钥匙,它解决了UI和业务分离的问题,但也带来了额外的学习成本。如果你的项目非常小型,可能并不需要一开始就上MVVM。但如果你们的App已经有十几个页面,并且业务逐渐复杂,那么越早做架构规划越好。
⏳ 架构升级需要时间和耐心
不要期待几天就能完成从MVC到MVVM的切换。我们花了三个月时间,边迭代边重构,最终才稳定下来。而且期间还经历了几次架构微调,比如引入协程、合并ViewModel等。
📖 文档和团队共识非常重要
我在项目初期没有及时输出架构说明文档,导致部分新人误用了老架构的方法。后来我们整理了一份《MVVM编码规范》,并在每周的代码评审中严格检查是否符合。
💡 别怕犯错,关键是持续改进
我们最初写的ViewModel也有不少反模式,后来才慢慢修正。重构就是一个不断优化的过程,重要的是保持代码干净、易于理解。
写在最后的一点感悟
做移动开发这些年,我越来越体会到:好的架构不是写出来的,而是在一次次踩坑中打磨出来的。MVVM虽然只是众多架构模式中的一种,但它教会了我如何写出更具伸缩性和可读性的代码。
现在的我们已经在逐步向Jetpack Compose + MVI架构过渡,但那又是另一个故事了。
感谢阅读,愿每一位开发者都能找到属于自己的代码之美。
文章作者简介:一名深耕Android领域八年的老兵,经历过创业公司、大厂以及出海项目实战。目前专注于架构优化与高性能客户端研发。欢迎在评论区交流你的实践经验~

评论 0