MVVM实战:在移动端构建可维护、可测试的架构

索引没建好
2025-06-22 13:59
阅读 375

引言:为什么是MVVM?

作为一名干了几年的全栈工程师,我参与过多个大型移动应用项目。从最初的MVC到后来的MVP,再到如今越来越流行的MVVM(Model-View-ViewModel),这些年来我也经历了不少“架构踩坑”的过程。

其中,有次让我印象最深的是我们在做一个金融类App的时候,整个团队被一个看似简单的“页面状态同步”问题折磨了好几天。最终通过重构使用MVVM架构,不仅解决了这个问题,还大幅提升了代码的可维护性和测试覆盖率。这也促使我去深入理解和实践MVVM,并将其作为我当前阶段首选的应用架构模式。

这篇文章不是一篇空泛的理论教程,而是基于我在实际项目中真实遇到的问题和解决方案,来聊聊我是如何在移动端使用MVVM架构的。希望通过这篇分享,能给正在学习或准备尝试MVVM的朋友一些启发。


项目背景与挑战

项目背景与挑战

我们当时负责开发一款面向企业用户的移动报销App,支持iOS和Android双平台。产品需要实现的功能包括表单填写、审批流程跟踪、发票上传、审批历史查看等。

起初,我们采用的是类似MVC的结构,也就是Activity/ViewController负责处理UI逻辑、网络请求、数据转换,甚至还掺杂了一些本地缓存操作。随着功能越来越多,代码也开始变得臃肿,出现了以下几个典型问题:

  1. 界面代码膨胀严重:Activity/ViewController里动辄上千行代码,各种异步回调交织在一起。
  2. 业务逻辑难以复用:同一个审批状态判断,在多个地方重复出现。
  3. 测试困难:因为所有逻辑都耦合在视图层,单元测试几乎无法开展。
  4. 状态同步混乱:比如用户点击了一个按钮,需要更新多个UI组件的状态,但往往只有部分更新,甚至出现不一致的情况。

特别是在多端并行开发时,安卓和iOS各自实现了大量类似的逻辑,导致后期修复问题时常常两边都要改,效率极低。


解决思路:引入MVVM架构

解决思路:引入MVVM架构

为了解决上述问题,我们决定在下一个版本开始逐步引入MVVM架构。

MVVM的核心思想是通过ViewModel隔离View和Model之间的交互。View只负责展示UI状态,而所有的业务逻辑、数据转换、事件处理都在ViewModel中完成。这样一来,就形成了清晰的分层结构。

对于我们来说,MVVM带来几个核心优势:

  • 解耦UI与业务逻辑:ViewModel不需要持有任何View的引用,因此更容易编写测试。
  • 跨平台复用:由于业务逻辑独立于视图,可以方便地复用在iOS和Android两端。
  • 响应式更新机制:借助LiveData(Android)或Combine(iOS),UI可以自动感知数据变化并更新,减少了手动控制状态同步的工作量。
  • 提升可测试性:ViewModel可以轻松进行单元测试,提高代码质量。

实践方案:MVVM结构拆解与模块化设计

实践方案: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
        }
    }


![原生应用架构-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062213/0d78142f-f687-4bf3-bd3e-c10875caecb8.jpg)


    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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝