移动应用架构设计:MVVM实战

单元测试补习生
2025-06-25 00:20
阅读 658

引言:为什么我会选择深入 MVVM 架构?

引言:为什么我会选择深入 MVVM 架构?

作为一名在互联网公司工作的移动端开发工程师,我经历了从 MVP 到 MVVM 架构的演变过程。记得刚接手第一个项目时,还是用传统的 MVC 模式写代码,随着业务越来越复杂,Activity/ViewController 的代码量也疯狂增长,测试难、维护难、耦合高成了家常便饭。

后来我们尝试引入了 MVVM(Model-View-ViewModel)架构模式,一开始只是抱着试试看的心态,没想到它不仅显著改善了代码结构,还提升了团队协作效率和开发体验。更重要的是,它让我们的应用更具可测试性和扩展性——这些正是现代移动开发中非常关键的素质。

在这篇文章中,我会结合我在一个实际项目中的经验,详细分享我是如何落地 MVVM 架构的,过程中踩过的坑、收获的心得,以及最终取得的效果。希望对大家有所启发。


项目背景与挑战

项目背景与挑战

去年我参与了一个内部孵化的社交类 App 开发项目,定位是一款轻量化、实时更新内容流的互动社区产品。初期版本需要支持用户登录、浏览内容卡片流、评论互动、消息推送等基本功能。技术选型方面,Android 使用 Kotlin + Jetpack 组件,iOS 使用 Swift + Combine(后续也有迁移到 SwiftUI 的尝试)。

当时最大的痛点是 页面逻辑复杂、数据流动不清晰。举个例子,比如内容详情页:

  • 数据来自多个接口(内容主体信息、用户信息、评论列表、是否点赞、关注状态)
  • 各种 UI 状态切换频繁(加载中、空数据、出错重试、正常显示)
  • 数据联动频繁(点赞后更新 UI 状态、新增评论后刷新列表)

最初我们没有明确的架构约束,各个页面的 ViewModel 都放在 Activity 中处理业务逻辑,导致页面臃肿,甚至出现重复代码的情况。

于是我们决定尝试采用 MVVM 架构重新设计页面结构,并结合 Android 的 ViewModel、LiveData 和 iOS 的 ObservableObject、Combine 来统一管理状态和数据流。


我们的解决方案:MVVM 架构实践

我们的解决方案:MVVM 架构实践

核心组件划分

我们以 Android 为例,构建 MVVM 的核心层如下:

  1. View 层(UI 层)

    • 对应 Activity / Fragment / Compose UI
    • 只负责渲染 UI 和响应用户事件,不做任何数据操作
  2. ViewModel 层

    • 负责持有 UI 相关的数据和状态
    • 将数据暴露为 LiveData 或 StateFlow,供 View 观察更新
    • 不持有任何 Context,不直接操作 View
  3. Repository 层

    • 封装数据源获取逻辑(本地缓存 + 网络请求)
    • 提供统一的数据访问接口给 ViewModel 调用
  4. UseCase 层(可选)

    • 复杂业务逻辑抽象成独立 UseCase 类,由 ViewModel 调用
    • 适用于多页面复用逻辑场景,如“发布内容”、“删除评论”

iOS 方面,使用类似的分层方式,结合 @StateObject@ObservedObjectNotificationCenterCombine 来实现双向绑定和异步通信。

示例:重构内容详情页

1. Repository 数据聚合

class ContentDetailRepository {

    suspend fun fetchContentDetail(contentId: String): Result<Content> {
        val content = apiService.getContentById(contentId)
        val comments = apiService.getCommentsByContentId(contentId)
        return Result.Success(
            ContentDetailData(content, comments)
        )
    }
}

2. UseCase 抽象业务逻辑

class LikeContentUseCase(private val repository: ContentDetailRepository) {
    suspend operator fun invoke(contentId: String): Result<Unit> {
        return repository.likeContent(contentId)
    }
}

3. ViewModel 管理状态

class ContentDetailViewModel : ViewModel() {

    private val _contentState = MutableLiveData<Content>()
    val contentState: LiveData<Content> get() = _contentState

    private val _commentList = MutableLiveData<List<Comment>>()
    val commentList: LiveData<List<Comment>> get() = _commentList

    fun loadContentDetail(contentId: String) {
        viewModelScope.launch {
            when (val result = repository.fetchContentDetail(contentId)) {
                is Result.Success -> {
                    _contentState.postValue(result.data.content)
                    _commentList.postValue(result.data.comments)
                }
                else -> {
                    // handle error
                }
            }
        }
    }

    fun toggleLike(contentId: String) {
        viewModelScope.launch {
            likeUseCase.invoke(contentId)
            updateLikeStatus(contentId)  // 本地更新UI
        }
    }
}

4. View 层观察 ViewModel 并更新 UI

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val viewModel = ViewModelProvider(this).get(ContentDetailViewModel::class.java)

    viewModel.contentState.observe(this, { content ->
        bindContentToView(content)
    })

    viewModel.commentList.observe(this, { list ->
        commentAdapter.submitList(list)
    })
}

这样拆分之后,UI 层只负责观察 ViewModel 提供的状态变化,而所有数据获取、状态变更都通过 ViewModel 控制,实现了很好的解耦。


踩坑经验总结

坑一:ViewModel 生命周期理解不清,导致内存泄漏

刚开始使用 ViewModel 时,有个误区是以为只要用了 ViewModel 就不会内存泄漏了。但我们在一个页面中不小心把网络请求回调持有了整个 ViewModel,结果发现页面关闭后 ViewModel 仍被 retain,最终查到是因为在协程中引用了上下文或全局单例对象未释放。

教训:不要在 ViewModel 中持有任何与生命周期相关的引用(Context、Application、Activity)。对于网络请求,在 ViewModel 作用域中发起,使用 viewModelScope 自动取消即可。


坑二:UI 状态同步混乱

早期我们尝试直接让 View 监听 Repository 层返回的数据结果,导致多个页面间数据状态不同步、加载动画重复触发等问题。

改进方案:将状态统一交给 ViewModel 管理,View 只订阅 ViewModel 提供的状态,避免各自监听底层资源。


坑三:iOS 上 Combine 的线程问题

在 iOS 侧我们最初使用了 Combine 进行数据流处理,但在并发操作时经常遇到 UI 更新异常的问题,例如:

  • 接收到异步数据后更新 @Published 变量时报错
  • 切换主队列失败,导致 UI 冻住

解决方法

  • 使用 .receive(on: DispatchQueue.main) 显式指定主线程更新
  • 使用 assign(to:) 时注意变量强弱引用,避免循环持有

效果与收益

重构后,整个项目的代码质量明显提升:

  • 模块职责清晰:各层边界明确,便于协作和交接
  • 减少重复代码:Repository 和 UseCase 的复用率大幅提升
  • 更容易测试:ViewModel 不依赖 UI,可以单独单元测试
  • 提升迭代效率:新需求接入成本更低,风险更小

上线后,我们也做了一些性能监控:

指标 改进前 改进后
页面首次加载耗时 900ms+ 750ms 左右
内存占用峰值 180MB 160MB
ANR 发生次数(日均) 8-10次 2-3次

可以看到,MVVM 架构不仅带来了良好的结构化优势,在性能层面也有一定正向作用。


实战心得与建议

✅ 架构不是越重越好

跨平台开发对比-2

在实际开发中,过度设计会带来不必要的复杂度。MVVM 更适合业务相对复杂的页面,或者需要长期维护的产品。对于小型 App 或实验性项目,KISS(Keep It Simple Stupid)原则依然适用。

✅ 数据驱动优先于 UI 操作

MVVM 的精髓在于用数据驱动 UI,而不是反过来。尽量避免直接在 ViewModel 中写 Toast.show、startActivity 等操作,而是通过 LiveData/State 暴露状态,UI 根据状态做出反应。

✅ 配套工具要跟上

  • 安卓端推荐使用 Hilt 做 DI
  • iOS 可搭配 Swinject 或 Resolver 等库注入依赖
  • 统一错误处理机制,避免每个 UseCase 单独 catch

✅ 注意跨平台一致性

如果我们同时开发双端 App,MVVM 的思想可以帮助我们保持架构一致性。虽然具体实现不同(Kotlin vs Swift),但可以制定统一的设计规范,方便两端协作和联调。

✅ 应用市场的适配经验

MVVM 在提升可测试性的同时,也能帮助我们在灰度发布、A/B 测试等运营场景下快速切换逻辑。比如我们曾将一个评论功能的入口开关封装到了 Repository 层,无需改 UI 即可动态控制。


结语:架构服务于人

移动端调试工具-1

回头来看,MVVM 的最大价值并不是它有多么炫技的技术点,而是让我们养成了状态管理和数据隔离的好习惯。它帮我们把“逻辑”和“展示”分离,“业务”和“界面”分层,这对提高开发效率、降低沟通成本至关重要。

架构本身只是一个工具,真正的关键是我们如何根据项目实际情况,灵活运用。就像开车一样,方向盘在你手上,路该怎么走,还得你自己判断。

如果你还在犹豫要不要尝试 MVVM,我的建议是:先从小项目练起,从一个小页面开始重构,感受一下它给你带来的便利。慢慢地你会发现,这是一条值得坚持的路。

希望这篇文章能帮你少走一些弯路,也欢迎你在评论区交流你的经验和疑问。我们共同进步!


文章作者:一位在一线互联网大厂搬砖多年的移动端开发者 📲
首发于个人博客 · 欢迎转载注明出处

评论 0

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