移动应用架构设计实战:MVVM的那些事儿

CtrlV艺术家
2025-06-20 09:33
阅读 597

大家好,我是一名有着五年移动开发经验的安卓工程师。这几年里,我参与过多个中大型项目的架构设计与实现,从最初的MVC到后来尝试MVP,再到如今的MVVM,一路踩了不少坑,也总结了一些经验和教训。

今天我想和大家分享一下我在一个实际项目中使用 MVVM(Model-View-ViewModel) 架构模式的一些实战经历。这并不是一篇高大上的理论文章,而是结合我自己亲身经历的一个真实项目背景、遇到的具体问题以及我们是如何一步步把这套架构落地的。


一、项目背景与痛点:为什么选MVVM?

一、项目背景与痛点:为什么选MVVM?

这个故事要从两年前的一次重构开始说起。

当时公司有一个老项目,是一个企业内部员工使用的工具类App,功能包括任务管理、审批流程、即时通讯等模块。代码已经运行了三年多,期间经历了多次迭代和多人维护。最开始是用的传统的MVC架构,随着业务复杂度上升,Activity/Fragment里面代码越来越臃肿,逻辑混乱,修改一个地方容易牵一发动全身。

更糟的是,测试非常困难。很多业务逻辑直接写在UI组件里,导致无法复用,也无法做单元测试。团队一度陷入“改个按钮颜色都要小心翼翼”的尴尬状态。

老板终于拍板:必须重构!而作为项目主力,我也被委以重任,负责整体技术架构的设计。


二、选择MVVM:不是跟风,是刚需

二、选择MVVM:不是跟风,是刚需

其实在那之前我已经接触过一些MVVM相关的概念,特别是Google推出的Jetpack库(比如ViewModel、LiveData)逐渐成熟,社区对MVVM也越来越推崇。

但我真正下定决心采用MVVM,是因为它恰好能解决我们当时面临的核心问题:

  1. UI层与业务逻辑耦合太强 → MVVM通过ViewModel解耦。
  2. 数据驱动视图,而不是反向操作 → LiveData天然支持观察者模式。
  3. 需要提高可测试性 → ViewModel可以脱离UI独立测试。
  4. 方便协作开发 → 各自专注职责,减少打架几率。

当然,也有人质疑说“MVVM是不是过于复杂?”,但在我看来,复杂的是业务本身,而不是架构本身。


三、实践过程:我们的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文件数量爆炸。后来我们采用了两个策略:

  1. 按功能模块归类,比如task/TaskListViewModel.ktprofile/UserInfoViewModel.kt
  2. 使用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来处理。


五、效果和收益:架构带来的改变

应用商店发布流程-1

重构完成后,整个项目的代码质量有了明显改善:

维度 重构前 重构后
代码可读性 糟糕,到处都是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

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