MVVM实战:在移动端构建可维护、可测试的架构
引言:为什么是MVVM?
作为一名干了几年的全栈工程师,我参与过多个大型移动应用项目。从最初的MVC到后来的MVP,再到如今越来越流行的MVVM(Model-View-ViewModel),这些年来我也经历了不少“架构踩坑”的过程。
其中,有次让我印象最深的是我们在做一个金融类App的时候,整个团队被一个看似简单的“页面状态同步”问题折磨了好几天。最终通过重构使用MVVM架构,不仅解决了这个问题,还大幅提升了代码的可维护性和测试覆盖率。这也促使我去深入理解和实践MVVM,并将其作为我当前阶段首选的应用架构模式。
这篇文章不是一篇空泛的理论教程,而是基于我在实际项目中真实遇到的问题和解决方案,来聊聊我是如何在移动端使用MVVM架构的。希望通过这篇分享,能给正在学习或准备尝试MVVM的朋友一些启发。
项目背景与挑战

我们当时负责开发一款面向企业用户的移动报销App,支持iOS和Android双平台。产品需要实现的功能包括表单填写、审批流程跟踪、发票上传、审批历史查看等。
起初,我们采用的是类似MVC的结构,也就是Activity/ViewController负责处理UI逻辑、网络请求、数据转换,甚至还掺杂了一些本地缓存操作。随着功能越来越多,代码也开始变得臃肿,出现了以下几个典型问题:
- 界面代码膨胀严重:Activity/ViewController里动辄上千行代码,各种异步回调交织在一起。
- 业务逻辑难以复用:同一个审批状态判断,在多个地方重复出现。
- 测试困难:因为所有逻辑都耦合在视图层,单元测试几乎无法开展。
- 状态同步混乱:比如用户点击了一个按钮,需要更新多个UI组件的状态,但往往只有部分更新,甚至出现不一致的情况。
特别是在多端并行开发时,安卓和iOS各自实现了大量类似的逻辑,导致后期修复问题时常常两边都要改,效率极低。
解决思路:引入MVVM架构

为了解决上述问题,我们决定在下一个版本开始逐步引入MVVM架构。
MVVM的核心思想是通过ViewModel隔离View和Model之间的交互。View只负责展示UI状态,而所有的业务逻辑、数据转换、事件处理都在ViewModel中完成。这样一来,就形成了清晰的分层结构。
对于我们来说,MVVM带来几个核心优势:
- 解耦UI与业务逻辑:ViewModel不需要持有任何View的引用,因此更容易编写测试。
- 跨平台复用:由于业务逻辑独立于视图,可以方便地复用在iOS和Android两端。
- 响应式更新机制:借助LiveData(Android)或Combine(iOS),UI可以自动感知数据变化并更新,减少了手动控制状态同步的工作量。
- 提升可测试性:ViewModel可以轻松进行单元测试,提高代码质量。
实践方案:MVVM结构拆解与模块化设计

为了更直观地说明我们是如何落地MVVM的,我以一个审批详情页为例来讲解。
该页面主要功能包括:
- 显示审批标题、状态、提交人
- 展示相关附件列表
- 提供按钮用于提交审批意见
- 实时显示审批状态变更(如:审核中 -> 已批准)
整体架构分层如下:
+------------------------+
| View |
| (Fragment/Activity |
| or ViewController) |
+-----------+------------+
|
+-----------v------------+
| ViewModel |
| (Shared Logic) |
+-----------+------------+
|
+-----------v------------+
| UseCase |
| (Business Logic) |
+-----------+------------+
|
+-----------v------------+
| Repository |
| (Network/Data Layer) |
+------------------------+
这种结构将各层职责划分得非常明确:
- View:只做UI渲染和事件监听;
- ViewModel:接收用户输入事件,调用UseCase获取数据并转换成UI可用的数据;
- UseCase:封装具体的业务规则;
- Repository:统一数据来源,屏蔽网络、数据库等细节。
这样的设计使整个系统具备良好的扩展性,同时避免了代码的腐化。
关键实现细节与代码示例

以下是我们使用Jetpack架构组件(Android)和SwiftUI + Combine(iOS)实现的部分关键代码片段。
Android端 ViewModel 示例(Kotlin + LiveData)
class ApprovalDetailViewModel : ViewModel() {
private val repository = ApprovalRepository()
// UI需要观察的数据源
val title = MutableLiveData<String>()
val status = MutableLiveData<String>()
val attachments = MutableLiveData<List<Attachment>>()
val isApproving = MutableLiveData<Boolean>(false)
fun loadApproval(approvalId: String) {
viewModelScope.launch {
val approval = repository.fetchApprovalById(approvalId)
title.value = approval.title
status.value = approval.status.toDisplayString()
attachments.value = approval.attachments
}
}

fun submitApproval(comment: String) {
isApproving.value = true
viewModelScope.launch {
try {
repository.submitComment(comment)
status.value = "已批准"
} finally {
isApproving.value = false
}
}
}
}
在这个ViewModel中,我们完全脱离了对View的依赖,所有的状态都可以通过LiveData被View订阅并自动刷新。
iOS端 ViewModel 示例(Swift + Combine)
import Combine
class ApprovalDetailViewModel: ObservableObject {
@Published var title: String = ""
@Published var status: String = ""
@Published var attachments: [Attachment] = []
@Published var isApproving: Bool = false
private let service = ApprovalService()
func loadApproval(withId id: String) {
service.fetchApproval(id: id) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let approval):
self.title = approval.title
self.status = approval.status.displayValue
self.attachments = approval.attachments
case .failure:
// 错误处理
break
}
}
}
func submitApproval(with comment: String) {
isApproving = true
service.submitComment(comment) { [weak self] success in
guard let self = self else { return }
if success {
self.status = "已批准"
}
self.isApproving = false
}
}
}
虽然语言不同,但整体结构是一致的,这为我们做跨平台一致性打下了良好基础。
遇到的坑和解决方式
坑1:过度暴露LiveData导致绑定混乱
初期我们尝试把每个字段都包装成LiveData,结果在View中要订阅太多字段,每次更新都需要重新绑定。后来我们采用了组合数据的方式,例如:
data class ApprovalUiState(
val title: String = "",
val status: String = "",
val canApprove: Boolean = false,
val isLoading: Boolean = false
)
然后只暴露一个LiveData<ApprovalUiState>,这样在View中只需要监听一次即可。
坑2:ViewModel生命周期管理问题
有些同学喜欢在ViewModel里持有Context或者做一些长时间运行的任务,导致内存泄漏。我们后来约定:ViewModel绝不持有任何Context对象,所有上下文相关的操作由View层负责。
此外,我们也统一使用viewModelScope(Android)和Cancellable(iOS)来做协程管理和取消订阅,确保没有遗漏。
坑3:跨平台模型差异处理不当
因为两个平台使用不同的JSON解析库(Android用Moshi,iOS用Codable),一开始经常遇到字段名匹配不一致的问题。后来我们统一定义IDL结构,自动生成Model代码,减少人为错误。
架构升级后的效果
自从全面转向MVVM后,我们的项目质量有了明显提升:
- 代码结构更清晰:每个人都能快速定位到自己负责的模块。
- 测试覆盖率从30%提到65%以上:ViewModel几乎100%覆盖,大大提高了稳定性。
- Bug数量明显下降:尤其是与UI状态相关的问题。
- 团队协作更加顺畅:前后端分离、iOS/Android协作效率高了很多。
- 发布流程更可控:因为核心逻辑已经抽象出UI层,可以在上线前做更充分的自动化验证。
另外,得益于良好的架构设计,我们后续接入了Flutter进行部分页面替换时,也能快速利用现有的ViewModel层逻辑,做到了平滑过渡。
经验总结与建议
如果你也正在考虑使用MVVM架构,以下几点经验或许对你有帮助:
✅ 真正理解“双向绑定”的本质
很多人以为MVVM就是让UI自动更新数据,其实更重要的是它带来了关注点分离的思想。你不用关心某个TextView怎么变颜色,而是应该思考“什么时候这个状态该变”。
✅ 尽早规划状态结构
提前设计好UIState数据结构非常重要,避免频繁修改带来的连锁反应。我们通常会在需求评审阶段就开始定义这些结构。
✅ 合理使用工具链
推荐配合使用:
- Android: ViewModel, LiveData, DataBinding + Kotlin 协程
- iOS: Combine + SwiftUI 或者 UIKit + ViewState 模式
- 跨平台通用:Retrofit / Alamofire / Moya等网络框架 + 共享的Domain Model
✅ 不要盲目追求“纯MVVM”
有时候我们会纠结:“是否每一个控件都要通过ViewModel绑定?”实际上,像简单的文本框输入(如搜索框),可以保持一定直接的View逻辑,反而更灵活。关键在于不要把复杂业务逻辑混入View层。
结语:架构从来不是一蹴而就的事情
MVVM也好,Clean Architecture也好,它们都只是手段,而不是目的。架构的本质,是要服务于团队协作、产品质量和持续交付的能力。
回想起当初那个为状态同步头疼的日子,现在再回头看,真的很庆幸当时做出了架构升级的决策。
希望我的这段实战经验,能帮你在移动开发的道路上少走些弯路。愿你也能够在每一次技术选型中,做出适合自己项目的正确选择。
如果你也在用MVVM或者有其他架构实践经验,欢迎留言交流,我们一起探讨更多可能性。

评论 0