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

TPS计算员
2025-06-29 14:43
阅读 674

开篇:为什么我会选择深入 MVVM

开篇:为什么我会选择深入 MVVM

作为在一线互联网公司从事移动端开发的工程师,我经历过从 MVP 到 MVVM 的迁移,也亲历了 Android 和 iOS 在架构层面的不断演化。在参与多个中大型项目之后,我发现 MVVM 已经成为一种非常主流、实用且可维护性极高的架构方式。

今天想跟大家聊聊我在一次实际项目中应用 MVVM 架构的经验和心得。不讲空泛的概念,只说真实的场景和问题,以及我们在工程实践中是怎么一步步踩坑、调整、优化并最终落地的。


项目背景:重构一个老项目的架构升级

项目背景:重构一个老项目的架构升级

去年我们团队接手了一个相对老旧的 App,功能模块较多,但代码结构混乱。最初采用的是 MVP 模式,但在后续迭代过程中逐渐暴露出几个明显的问题:

  • Activity/ViewController 承担了大量的业务逻辑,越来越臃肿
  • Presenter 层与 View 层耦合较深,难以复用和测试
  • 数据状态管理混乱,页面重建时容易出现 UI 错乱
  • 多人协作下缺乏统一规范,导致不同模块风格差异大

为了支持长期演进和提升开发效率,我们决定对整个项目进行架构重构,目标是迁移到 MVVM + Jetpack(Android)/Combine + SwiftUI(iOS),同时保留原有功能,分阶段上线。


遇到的挑战:不是所有组件都适合“一刀切”

刚开始推行的时候,我们遇到的最大阻力其实是来自于“习惯” —— 包括我自己。

举个例子:
原本有个首页 Feed 流页面,里面有数据加载、下拉刷新、上滑缓存预加载等多个复杂交互。之前 MVP 的实现是每个事件触发后直接调用 Presenter 接口,然后回调更新 UI。

现在改成 MVVM 后,我们尝试把 ViewModel 变成纯粹的状态持有者,View 观察它的 LiveData 或 ObservableObject,通过绑定机制自动刷新 UI。

但问题来了:
部分复杂的交互(比如手势、动画)不好通过绑定机制响应实时变化;一些非 UI 相关的副作用(例如埋点上报、Toast 提示等)也不方便放进 ViewModel 里。

这时候我们就意识到:
MVVM 并不是万能的,它更像是一种理念,而不是固定模板。

我们需要在架构设计中做取舍,甚至根据业务特征混用不同的模式。


我们采用的技术方案

Android 端:Jetpack + 单向绑定 + Clean Architecture 分层

  • ViewModel + LiveData: 控制 UI 状态流
  • Repository 层: 统一数据来源(本地 Room + 远程 API)
  • UseCase (Interactor): 将业务逻辑抽离为可复用的操作单元
  • DataBinding + ViewBinding: 减少 findViewById 的侵入性代码
  • Navigation Component: 统一页面跳转逻辑
class HomeViewModel : ViewModel() {
    private val repository = HomeRepository()

    val feedItems = MutableLiveData<List<FeedItem>>()
    val loading = MutableLiveData<Boolean>()

    fun fetchFeeds() {
        loading.value = true
        viewModelScope.launch {
            try {
                val data = repository.getFeeds()
                feedItems.postValue(data)
            } catch (e: Exception) {
                // handle error, maybe trigger Toast via Event Bus
            } finally {
                loading.postValue(false)
            }
        }
    }
}

注意:早期我们曾尝试用 LiveData 暴露过多状态,后来发现这样反而增加了维护成本。于是改用 “单向绑定 + ViewState 模式”,让 ViewModel 更专注状态而非行为。


iOS 端:SwiftUI + Combine + Clean Architecture

虽然团队主力是 Android 方向,但我们也在同步推进 iOS 的跨平台适配。对于 iOS 而言,我们采用了 Apple 主推的 SwiftUI + Combine 架构组合。

核心思路:

  • 使用 @ObservedObject 作为 ViewModel 的桥梁
  • 借助 Combine 实现响应式数据流
  • 对网络请求使用封装良好的异步操作库(如 Moya)
class HomeViewModel: ObservableObject {
    @Published var feeds: [FeedItem] = []
    @Published var isLoading = false
    
    private let service = HomeService()
    
    func loadFeeds() {
        isLoading = true
        service.fetchFeeds { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success(let items):
                    self?.feeds = items
                case .failure(let error):
                    // Handle error with alert or event bus
                    print("Error fetching feeds: $error)")
                }
                self?.isLoading = false
            }
        }
    }
}

在 UI 层则通过 .sheet.alert 等来处理弹窗、提示等副作用,避免直接在 ViewModel 中写这些逻辑。


踩过的坑 & 对应解决方案

1. ViewModel 泄漏问题

初期我们没注意生命周期控制,某些 ViewModel 里面注册了全局监听器(如 EventBus),导致内存泄漏严重。

✅ 解决办法:

  • Android:使用 viewModelScope + SavedStateHandle 管理状态和协程生命周期
  • iOS:在 deinit 中清理订阅和回调引用,使用 weak self 避免循环强引用

2. 多种数据源如何优雅聚合?

项目中有多个数据源,包括数据库、本地缓存、远程 API、WebSocket 推送等。

✅ 最终我们采用 Repository 模式,在抽象层统一对外暴露接口,屏蔽具体的数据来源细节。

interface FeedRepository {
    suspend fun getLocalFeeds(): List<Feed>
    suspend fun fetchFromNetwork(): List<Feed>
    suspend fun saveToCache(feed: Feed)
}

通过这种方式,不管是 ViewModel 还是 UseCase 都可以以一致的方式访问数据。


3. 页面重建时状态丢失?

MVVM 很强调状态驱动 UI,那如果用户旋转屏幕或切后台再回来呢?

✅ 我们引入了两种方案:

  • Android:使用 SavedStateHandle + ViewModelStoreOwner 自动保存基础状态
  • iOS:使用 @AppStorageUserDefaults 临时缓存关键状态,结合 SwiftUI 生命周期回调

4. 如何处理非 UI 行为?(如 Toast、Snackbar、埋点)

这个问题在 MVVM 中属于“边缘地带”。ViewModel 不应该直接持有 View 引用,也无法直接操作 UI。

✅ 我们的做法是:

  • 使用 SingleLiveEvent(Android)或自定义 EventBus 发送轻量级事件通知,交由 UI 层监听处理
  • 埋点尽量下沉到 Repository 层或 UseCase,避免污染 UI 层
  • 公共的 UI 提示统一封装成工具类,由 BaseActivity/BaseView 调用

效果与收益

经过两三个月的努力,整个项目完成了核心页面的迁移。我们也逐步体会到 MVVM 架构带来的显著好处:

  1. UI 与业务逻辑解耦清晰,View 只负责渲染,ViewModel 只负责状态管理
  2. 便于单元测试,因为 ViewModel 本身就是无 UI 的纯对象
  3. 提高复用能力,比如同一个 ViewModel 可供多个 UI 组件消费
  4. 支持更好的协同开发体验,各层级边界明确,命名统一,新人更容易上手

特别值得一提的是,我们在重构过程中建立了一套通用的 Base 类和模板,大大提升了后续新页面开发的效率。


经验分享:几点建议送给大家

如果你也在考虑或者正在实践 MVVM 架构,不妨看看这些建议:

✅ 1. 从“关注状态”出发,而不是“绑定一切”

不要试图把所有东西都丢给 ViewModel,尤其是一些不适合放进去的东西(比如手势、过渡动画)。要敢于在需要的时候写一点“非 MVVM”的代码。

✅ 2. 合理设计层级边界,别搞太复杂

Clean Architecture 是好东西,但也别盲目追求“五层八层”的模型。按需拆分,初期保持简单,后期再根据复杂度做扩展即可。

✅ 3. 多利用语言特性简化代码

无论是 Kotlin 的协程、委托属性,还是 Swift 的 property wrapper、Combine 操作符,都是非常好用的语言特性。善用这些工具可以让 MVVM 的实现更简洁有力。

✅ 4. 不要忽视平台差异

Android 和 iOS 在数据绑定机制和生命周期管理上有较大差异。比如 iOS 的 SwiftUI 本身已经内置很多状态管理能力,不需要完全模仿 Android 的 LiveData 模型。

✅ 5. 性能 & 用户体验不能忽视

MVVM 再好,也不能牺牲性能。我们在迁移到 SwiftUI 的时候就遇到了列表滚动卡顿的问题,最终通过懒加载和 cell 复用策略得以解决。


结语:MVVM 是一种思维方式,而不是硬性规范

回过头来看,这次架构升级不仅仅是一次技术上的迁移,更是我们对移动开发理解的一次升华。它让我们更加注重职责分离、状态管理和协作规范,也为后续的可持续发展打下了基础。

当然,没有完美的架构,只有更适合当前场景的架构。就像我们在实践中学会灵活切换不同模式一样,真正的工程师应该是懂得权衡与变通的人。

希望这篇来自真实战场的记录,能对你有些启发。如果你也在用 MVVM,或者正准备开始,欢迎留言交流你的想法和经验,我们一起进步 👨‍💻


本文内容基于笔者在某头部社交类 App 团队的实际工作经验整理撰写,如有雷同纯属巧合。文中所涉及架构已通过灰度发布验证并在生产环境中稳定运行半年以上。

评论 0

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