移动应用架构设计实战:MVVM的那些事儿
大家好,我是一名有着五年移动开发经验的安卓工程师。这几年里,我参与过多个中大型项目的架构设计与实现,从最初的MVC到后来尝试MVP,再到如今的MVVM,一路踩了不少坑,也总结了一些经验和教训。
今天我想和大家分享一下我在一个实际项目中使用 MVVM(Model-View-ViewModel) 架构模式的一些实战经历。这并不是一篇高大上的理论文章,而是结合我自己亲身经历的一个真实项目背景、遇到的具体问题以及我们是如何一步步把这套架构落地的。
一、项目背景与痛点:为什么选MVVM?

这个故事要从两年前的一次重构开始说起。
当时公司有一个老项目,是一个企业内部员工使用的工具类App,功能包括任务管理、审批流程、即时通讯等模块。代码已经运行了三年多,期间经历了多次迭代和多人维护。最开始是用的传统的MVC架构,随着业务复杂度上升,Activity/Fragment里面代码越来越臃肿,逻辑混乱,修改一个地方容易牵一发动全身。
更糟的是,测试非常困难。很多业务逻辑直接写在UI组件里,导致无法复用,也无法做单元测试。团队一度陷入“改个按钮颜色都要小心翼翼”的尴尬状态。
老板终于拍板:必须重构!而作为项目主力,我也被委以重任,负责整体技术架构的设计。
二、选择MVVM:不是跟风,是刚需

其实在那之前我已经接触过一些MVVM相关的概念,特别是Google推出的Jetpack库(比如ViewModel、LiveData)逐渐成熟,社区对MVVM也越来越推崇。
但我真正下定决心采用MVVM,是因为它恰好能解决我们当时面临的核心问题:
- UI层与业务逻辑耦合太强 → MVVM通过ViewModel解耦。
- 数据驱动视图,而不是反向操作 → LiveData天然支持观察者模式。
- 需要提高可测试性 → ViewModel可以脱离UI独立测试。
- 方便协作开发 → 各自专注职责,减少打架几率。
当然,也有人质疑说“MVVM是不是过于复杂?”,但在我看来,复杂的是业务本身,而不是架构本身。
三、实践过程:我们的MVVM是怎么搭起来的

下面这部分我会结合我们当时的结构设计来详细讲讲。
1. 整体结构划分
我们按照官方推荐的方式将整个App分成以下几个层级:
UI Layer (View + Fragment / Activity)
|
ViewModel Layer
|
Repository Layer
|
Data Layer (Local DB / Network API / Preferences)
每个层级只与下一层交互,不跨层调用,形成清晰的边界。
- View:纯粹处理UI展示和用户交互。
- ViewModel:持有View的状态,封装UI相关逻辑,不依赖任何Android Framework类。
- Repository:统一数据源访问入口,决定是从本地缓存取还是网络拉取。
- Data Source:包括Room数据库、Retrofit接口、SharedPreferences等。
Tip:一开始团队成员普遍不习惯这种分层方式,总觉得“代码变多了”。但当某个需求频繁改动时,他们才意识到“原来代码也能长出结构感”。
2. 视图层怎么写才优雅?
这里我想强调一点:不要让XML或者ViewBinding成为你偷懒的借口。
虽然有了ViewBinding后不用再findViewById了,但别忘了我们还要控制UI更新逻辑。所以我们在视图层做了几件事:
- 所有点击事件绑定在ViewModel中(借助
android:onClick="@{viewModel.someCommand}") - 使用LiveData配合Observer来驱动页面刷新,避免直接操作View状态
- 避免在Fragment中处理业务逻辑,只监听ViewModel变化并更新UI
举个例子,在点击按钮触发某个网络请求时,我们在布局文件这样写:
<Button
android:id="@+id/btn_refresh"
android:text="刷新"
android:onClick="@{() -> viewModel.refreshData()}"
/>
而在Fragment里只需要订阅结果:
viewModel.data.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
3. ViewModel如何承载真正的“状态”?
很多人以为ViewModel就是放一些回调方法而已,其实不然。
我倾向于将ViewModel看作是“屏幕级别的状态容器”。比如,当你切换Tab、旋转屏幕时,ViewModel应该记住当前页的状态(加载中、已加载、错误等),并通过暴露LiveDatas来通知视图改变。
我们项目中的一个典型ViewModel结构如下:
class TaskDetailViewModel : ViewModel() {
private val _task = MutableLiveData<Task>()
val task: LiveData<Task> = _task
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
fun fetchTask(taskId: String) {
_isLoading.value = true
repository.fetchTask(taskId) { result ->
_isLoading.postValue(false)
if (result.isSuccess()) {
_task.postValue(result.data)
} else {
// 错误处理交给Snackbar或Toast
_errorEvent.postValue("加载失败,请重试")
}
}
}
private val _errorEvent = SingleLiveEvent<String>()
val errorEvent: LiveData<String> = _errorEvent
}
注意这里用了SingleLiveEvent来防止事件被重复消费,这对于错误提示、导航跳转等场景非常重要。
4. Repository该怎么设计?
这个问题曾困扰我一段时间。最初我们是让ViewModel直接调用Retrofit服务,结果发现一旦数据来源变了(比如先网络、后缓存),ViewModel也要跟着改。
于是我们引入了一个统一的Repository层,并让它去封装所有的数据获取细节。
示例:
class TaskRepository(private val apiService: ApiService, private val localStore: LocalStore) {
fun fetchTask(taskId: String, callback: ResultCallback<Task>) {
if (NetworkUtils.isOnline()) {
apiService.getTask(taskId).enqueue(object : Callback<TaskResponse> {
override fun onResponse(...) {
val task = convertToTask(response.body())
localStore.saveTask(task)
callback.onSuccess(task)
}
override fun onFailure(...) {
callback.onError()
}
})
} else {
val cached = localStore.getTask(taskId)
callback.onSuccess(cached)
}
}
}
这样的好处很明显:上层逻辑完全不知道具体数据来源是哪,即使明天改成WebSocket也不会影响UI层。
四、遇到的挑战与解决过程

虽然MVVM理论上很完美,但真正在项目落地的时候我们也遇到了不少挑战:
挑战一:如何组织大量ViewModel?
刚开始为了图方便,每个页面都单独建一个ViewModel,很快ViewModel文件数量爆炸。后来我们采用了两个策略:
- 按功能模块归类,比如
task/TaskListViewModel.kt、profile/UserInfoViewModel.kt; - 使用ViewModelProvider.Factory自定义构造器,注入依赖项(如Repository);
另外还引入了Koin/Dagger来辅助依赖注入,提升可测试性和可维护性。
挑战二:页面间通信怎么办?
这是MVVM的一个常见难题。原本想用EventBus,但违背了“单一数据源”的原则。
我们最终采用了Google官方推荐的 SharedViewModel + Navigation Graph 的方式:
- 在Navigation Graph中指定SharedViewModel
- 多个Fragment共享同一个ViewModel实例
- 用MutableLiveData作为通信桥梁
比如A Fragment发送一个事件给B Fragment:
// A Fragment中
sharedViewModel.sendAction(UPDATE_LIST)
// B Fragment中
sharedViewModel.action.observe(viewLifecycleOwner) {
when(it) {
UPDATE_LIST -> reloadData()
}
}
挑战三:生命周期管理难搞
LiveData默认跟随生命周期感知,在observe的时候传入viewLifecycleOwner就OK。
不过有些场景可能需要忽略生命周期感知,比如某些后台任务完成之后通知UI,不管当前页面是否可见。这时我们就得手动用postValue()或者结合协程+lifecycleScope来处理。
五、效果和收益:架构带来的改变

重构完成后,整个项目的代码质量有了明显改善:
| 维度 | 重构前 | 重构后 |
|---|---|---|
| 代码可读性 | 糟糕,到处都是if-else嵌套 | 分层清晰,逻辑分离 |
| 测试难度 | 几乎为0 | ViewModel可做单元测试 |
| 修改风险 | 动一个逻辑可能导致多个页面异常 | 影响范围明确 |
| 开发效率 | 增加功能常常需要重头看一遍代码 | 可快速复用已有逻辑 |
| 性能 | 内存泄漏频发 | ViewModel自动清理资源 |
特别要说的一点:性能提升显著!
通过良好的架构组织,避免了冗余的数据请求和不必要的对象创建。例如使用LiveData后,只有UI处于活动状态时才会收到数据变化通知,减少了CPU消耗。
而且我们引入了Room数据库后,页面首次打开几乎都能做到秒开(命中缓存),大大提升了用户体验。
六、一些小插曲和感悟
记得有一次,我们要上线一个新功能,时间紧任务重。一位新来的同事抱怨说:“你们这个架构太啰嗦了,能不能直接在Fragment里调接口算了?”
我当时没有反驳他,而是让他自己试着写了几个页面。一周之后他自己主动来找我,说“我现在知道为什么要这么分层了”。
还有一次发布版本时,我们在CI环节加入了ArchUnit检查,确保不能出现View直接引用Model的情况。没想到第一次跑就报了几十个违规项……但我们坚持修完了所有警告,整个团队也渐渐习惯了这套约束。
这些小事让我更加确信:
软件架构不是用来装门面的,它是一种“无形”的生产力投资。
七、一些经验建议
如果你也在考虑使用MVVM,或者已经开始使用了,以下是我总结的一些个人建议,供你参考:
✅ 不要盲目追求“纯正MVVM”
有时候为了简化逻辑,允许少量UI逻辑存在在Fragment中是可以接受的,只要不影响整体结构即可。
✅ 把ViewModel当作状态容器,而非业务处理器
不要在里面写太多if-else判断,尽量委托给Repository或UseCase。
✅ 结合协程/Flow来处理异步逻辑
LiveData很好用,但在处理链式调用、合并多个数据源等场景不如Flow灵活。
✅ 统一错误处理机制
可以设计一个统一的Error Handler来捕获异常,避免每次都要弹Toast、记录日志写两遍。
✅ 引入架构检测工具(如ArchUnit)
这能帮助团队保持一致性,避免架构退化。
✅ 注意适配不同平台(尤其iOS端)
如果你想用MVVM做跨平台,要注意iOS中没有ViewModel的概念,可以借助SwiftUI的ViewModel风格或RxSwift模拟类似结构。
八、结语:架构从来都不是万能药
MVVM不是银弹,但它确实帮我们走出了一段技术债的泥潭。更重要的是,它让我们团队形成了一个基本的工程共识和规范,这才是最大的收益。
如果你现在正面临着类似的问题,不妨大胆尝试一下MVVM。哪怕前期学习成本稍高,但从长期来看,绝对是值得的投入。
最后送给大家一句我在开发中最常提醒自己的话:
“写代码是为了让人读,偶尔给机器跑。”
——《程序员修炼之道》
希望这篇来自一线实战的小分享,能对你有一点启发。
有任何疑问或补充,欢迎留言交流!

评论 0