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

♂郭军
2025-06-22 08:12
阅读 476

开篇:为什么是 MVVM?

开篇:为什么是 MVVM?

作为一名在互联网公司工作的移动开发工程师,我这些年几乎经历过所有的主流 Android 架构模式。从最开始的 MVC,到 MVP,再到现在的 MVVM,每一步演进背后都是一段与代码复杂性、团队协作和可维护性作斗争的过程。

今天想聊聊我在一个真实项目中使用 MVVM(Model-View-ViewModel) 的实战经验,包括为什么要选择它、遇到的问题、怎么解决的,以及最终带来的收益。希望这篇笔记能给正在为架构选型头疼的同学一点参考。


问题描述:老项目陷入“逻辑泥潭”

问题描述:老项目陷入“逻辑泥潭”

去年我们接手了一个中等规模的老项目,主要功能是一个内容社区 App,用户可以浏览图文内容、发布动态、评论互动等。虽然整体功能已经上线运营了一段时间,但技术债堆积严重。

具体表现有几个方面:

  1. Activity/Fragment 职责混乱:很多页面的业务逻辑直接写在 Activity 中,动辄上万行代码。
  2. 界面与数据耦合严重:UI 层直接依赖 Repository 和网络请求结果,调试和测试非常困难。
  3. 团队协作成本高:多人修改同一个文件容易冲突,Review 也难以下手。
  4. 单元测试基本没有覆盖:所有业务都在 UI 层,Mock 数据困难,导致质量保障薄弱。

面对这些问题,我们意识到必须对整个项目的架构进行一次升级重构,不能再继续“缝缝补补又一年”。


解决方案:引入 MVVM,解耦视图与逻辑

为什么是 MVVM?

MVVM 的核心在于 职责分离数据驱动视图。通过 ViewModel 管理 UI 相关的数据状态,结合 LiveData 或 RxJava 实现观察者模式,我们可以做到:

  • View 只负责展示
  • ViewModel 管理 UI 相关的状态变化
  • Model 处理数据获取和持久化

这种结构特别适合我们在团队开发中明确分工,并且支持后续的自动化测试和组件复用。

我们的架构设计

在实际落地过程中,我们做了一些定制化的适配:

App Module
├── ui
│   ├── feed
│   │   ├── FeedFragment.kt
│   │   ├── FeedAdapter.kt
│   ├── detail
│       ├── DetailActivity.kt
├── viewmodel
│   ├── FeedViewModel.kt
│   ├── DetailViewModel.kt
├── repository
│   ├── ContentRepository.kt
├── model
│   ├── ContentItem.kt
├── network
│   ├── ApiService.kt
└── util
    └── ...

在这个基础上,我们还使用了如下几个关键组件和技术栈:

  • AndroidViewModel + SavedStateHandle:用于处理生命周期感知的数据保存与恢复。
  • LiveData + Transformations:用来暴露 UI 需要的状态数据。
  • Repository 模式:统一管理本地数据源(Room)和远程数据源(Retrofit)。
  • Koin 或 Dagger(视项目而定):注入 ViewModel 与 Repository。
  • 协程 + Flow:异步任务与响应式数据流的首选方式。

代码实践:以内容首页为例

这里我们以 App 首页 Feed 流模块为例,来讲解一下 MVVM 在实际项目中的结构和实现方式。

1. Model 层定义数据实体

data class FeedContent(
    val id: String,
    val title: String,
    val author: String,
    val imageUrl: String,
    val likes: Int,
    val comments: Int
)

2. Repository 获取数据

class FeedRepository @Inject constructor(
    private val remoteDataSource: FeedRemoteDataSource,
    private val localDataSource: FeedLocalDataSource
) {
    suspend fun fetchLatestFeed(): List<FeedContent> {
        val data = remoteDataSource.fetchFeedFromNetwork()
        // 可以缓存到本地数据库或 SharedPreferences
        return data
    }
}

3. ViewModel 处理业务逻辑

应用性能监控-2

class FeedViewModel @ViewModelInject constructor(
    private val repository: FeedRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _feedData = MutableLiveData<List<FeedContent>>()
    val feedData: LiveData<List<FeedContent>> = _feedData

    private val _loading = MutableLiveData<Boolean>()
    val loading: LiveData<Boolean> = _loading

    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error

    fun loadFeed() {
        viewModelScope.launch {
            _loading.value = true
            try {
                val result = repository.fetchLatestFeed()
                _feedData.value = result
            } catch (e: Exception) {
                _error.value = "加载失败"
            } finally {
                _loading.value = false
            }
        }
    }
}

4. View 层绑定 ViewModel

class FeedFragment : Fragment() {

    private val viewModel: FeedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val adapter = FeedAdapter()
        binding.feedRecycler.adapter = adapter

        viewModel.feedData.observe(viewLifecycleOwner, { items ->
            adapter.submitList(items)
        })

        viewModel.loading.observe(viewLifecycleOwner, { isLoading ->
            binding.progressBar.isVisible = isLoading
        })

        viewModel.error.observe(viewLifecycleOwner, { errorMsg ->
            Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
        })

        if (savedInstanceState == null) {
            viewModel.loadFeed()
        }
    }
}

这样我们就完成了从数据获取、处理到 UI 更新的一整套流程,逻辑清晰,结构分明。


踩坑经验:实战中遇到的真实问题

任何架构都不是银弹,在实际推进 MVVM 的过程中我们也踩了不少坑。

坑点一:ViewModel 生命周期理解不透彻

我们曾经在一个页面里在 onCreate 中初始化数据,但在旋转屏幕时重复调用加载接口,造成多次网络请求。

解决方案:充分利用 viewModelScopeSavedStateHandle 来保留中间状态,避免重复加载。

坑点二:LiveData 的更新方式不当引发性能问题

早期有人习惯在 ViewModel 中使用 MutableLiveData 暴露出来让 View 修改状态,这实际上违反了数据不可变原则,还容易导致内存泄漏。

正确姿势:只暴露 LiveData,并通过封装方法间接改变内部状态。

坑点三:多个 Fragment 共享 ViewModel 出现数据污染

我们在两个相关页面共享一个 ViewModel 后,出现了互相干扰的问题。

解决办法:合理利用 by viewModels()by activityViewModels() 控制作用域范围,或者拆分成子 ViewModel。


效果总结:重构之后的变化

经过三个月的持续重构与新功能开发并行推进,我们的 App 在以下几个维度发生了明显变化:

维度 改造前 改造后
页面层级结构 混乱,单个类动辄几千行 层级清晰,每个类职责单一
团队协作效率 多人开发频繁冲突 明确边界后协作顺畅
单元测试覆盖率 不足 10% 提升至 65%+
Bug 定位速度 通常需要走完整路径才能发现 可快速定位逻辑错误
新人入门难度 文档缺失、代码难读 结构统一,文档易写

尤其让人欣慰的是,原本需要两周上线的新功能,现在一周内就能稳定交付,迭代节奏明显加快。


经验分享:我的几点建议

原生应用架构-1

1. 架构不是越先进越好,合适才是王道

MVVM 是目前 Android 官方推荐架构之一,但在一些小型项目或者实验性质的项目中,不一定非要强推。有时候 MVP 更轻量,甚至简单的分层也能满足需求。

2. ViewModel 并不是万能的“救命稻草”

不要把 ViewModel 当成第二个 Activity,里面照样不能放太多业务细节。该拆的业务逻辑还是得抽出去,比如放到 UseCase 或 Interactor 层。

3. 注意不同平台的适配差异

我们在一个跨平台项目中同时使用 Jetpack Compose 和 Flutter,发现部分 ViewModel 状态管理和生命周期存在不一致问题。因此要特别注意跨平台场景下的状态同步策略。

4. 关注用户体验与性能优化

在使用 LiveData 和 Flow 的时候,如果频繁触发刷新,很容易引起 UI 抖动。这时候可以通过 debounce、distinctUntilChanged、防抖合并等方式优化。

例如:

viewModel.feedData
    .debounce(300)
    .distinctUntilChanged()
    .observe(...)

还能有效减少重复渲染。

5. 别忘了发版过程中的“隐形成本”

MVVM 结构良好也方便我们在 App 发布阶段更好地配合灰度测试和埋点分析。由于数据层与 UI 层彻底分离,可以在 Repository 层加一层“日志装饰器”,记录每一次数据变更和错误来源,极大提升了排查效率。


尾声:写给自己和未来

MVVM 不是终点,也不应该是我们追求的唯一目标。它只是帮助我们写出更高质量代码的一种工具和思维方式。

回过头来看这段重构之路,其实收获最大的并不是技术上的提升,而是团队对于“代码整洁”这件事达成了共识。大家不再纠结于某个函数到底写在哪,更多讨论的是如何将逻辑抽象出来,如何让下一位接手的人更容易理解。

也许这才是真正的工程思维。


如果你也在考虑使用 MVVM 或者已经在使用了,欢迎留言交流。愿我们在技术的路上少些焦虑,多些笃定。

评论 0

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