移动应用架构设计实战:我在MVVM实践中踩过的坑和收获

谢思远_码农
2025-06-18 23:33
阅读 740

去年,我接手了一个中型的移动应用项目,目标是在3个月内完成一个稳定、可扩展、用户体验良好的跨平台App。当时我们的团队刚刚从传统的MVC结构转向现代架构模式,选择了MVVM(Model-View-ViewModel)作为主框架

说实话,在一开始我并没意识到这个选择意味着什么。只是单纯觉得:“MVVM听起来挺高级的,数据绑定听着很舒服”。后来才知道,真正落地的时候,这是一场技术与认知双重层面的洗礼。

今天我想跟大家分享一下我们在这个过程中是怎么从混乱走向有序的,也希望能帮助到那些正在转型或者准备转型使用MVVM的朋友少走些弯路。


一、项目背景:一场“重构”引发的“战争”

一、项目背景:一场“重构”引发的“战争”

我们的应用原本是一个基于传统MVC结构的老项目,主要功能是面向B端用户的任务管理和工单系统。随着业务逻辑复杂度不断增加,代码越来越臃肿,“谁修改了哪个状态”、“点击按钮后到底干了啥”变成了开发的噩梦。

老问题一览:

  • Activity/ViewController里混杂了网络请求、UI更新、数据处理;
  • 数据状态难以追踪,经常出现“视图显示错乱但查不出根源”的情况;
  • 测试困难,耦合度高导致几乎无法做自动化测试;
  • 新人上手成本高,没人敢轻易改动核心模块。

面对这些痛点,我们在一次技术评审会上一致决定:全面引入MVVM架构,并配合Jetpack组件重构Android端代码(iOS则同步进行SwiftUI+Combine方案试点)。目标很明确:提高代码可维护性、增强可测性、减少耦合


二、初探MVVM:理想丰满,现实骨感

二、初探MVVM:理想丰满,现实骨感

MVVM的核心理念其实并不难理解:把UI(View)和数据模型(Model)之间的通信通过ViewModel来解耦,实现双向绑定。理论上,这样可以让View不依赖于具体的数据源,也让业务逻辑不再直接操作界面。

但在实际编码阶段,我们遇到了几个致命的问题:

1. ViewModel怎么写?

起初我们照着官方文档写了几个例子,却发现一旦涉及复杂的页面交互和数据流,根本无从下手。“应该放在ViewModel里的到底是数据本身,还是方法?”、“ViewModel能持有Repository吗?”——这些问题没有明确答案,大家各有各的理解,最终写出了一堆风格迥异的代码。

2. LiveData vs RxJava?

我们原生采用的是Jetpack的LiveData,但部分老代码用了RxJava。两种观察者机制混在一起,让开发者在调试时完全摸不着头脑。特别是生命周期感知方面,一度出现了内存泄漏和空指针的“重灾区”。

3. UI层如何响应变化?

虽然有androidx.lifecycle.ViewModelLiveData,但我们对“如何将这些状态安全地绑定到UI”缺乏实践经验。比如,在列表滚动时频繁触发刷新导致性能问题,或者在数据未完全加载前就尝试绑定视图造成崩溃。

那段时间,每天早上的Standup都像是“问题发布会”,大家围坐一圈轮流讲昨晚又发现了哪些“诡异的现象”。


三、破局之路:我们是如何一步步找到节奏的?

三、破局之路:我们是如何一步步找到节奏的?

在经历几次重构失败之后,我们终于开始沉淀出一套适用于自己项目的MVVM最佳实践方式。下面是我总结的一些关键点和经验教训。

✅ 架构分层清晰化

我们将整个架构分为如下层级:

View Layer (Activity/Fragments)
    ↓
ViewModel Layer (负责UI相关的状态和行为)
    ↓
Repository Layer (统一数据来源接口)
    ↓
Data Source Layer (本地DB / 网络API / 文件存储等)
  • ViewModel只暴露LiveData<UIState>供View观察;
  • Repository负责协调多个数据源;
  • 所有网络或数据库访问都封装在DataSource中;
  • View不做任何状态判断,所有逻辑都在ViewModel中处理。

这一步是我们架构标准化的关键,从此再也不用担心“谁该调哪个API”这种低级争议。

✅ 统一状态模型 + Single Source of Truth

我们定义了一个通用的UIState类,用来统一封装各种UI所需的状态信息:

sealed class UIState<out T> {
    data class Success<out T>(val data: T) : UIState<T>()
    data class Error(val exception: Exception) : UIState<Nothing>()
    object Loading : UIState<Nothing>()
}

然后每个ViewModel对外暴露的都是LiveData<UIState<...>>类型:

class TaskListViewModel : ViewModel() {
    private val _tasks = MutableLiveData<UIState<List<Task>>>()
    val tasks: LiveData<UIState<List<Task>>> = _tasks

    fun loadTasks() {
        viewModelScope.launch {
            try {
                _tasks.value = UIState.Loading
                val result = repository.getTasks()
                _tasks.value = UIState.Success(result)
            } catch (e: Exception) {
                _tasks.value = UIState.Error(e)
            }
        }
    }
}

这种方法不仅提升了代码一致性,也让UI状态更容易被观察和处理。

✅ 使用协程统一异步流程

Kotlin Coroutines极大地简化了异步编程。我们把所有的网络请求和本地数据读取都迁移到了协程中,并结合viewModelScope来确保生命周期内自动取消任务。

对于iOS端我们使用了Combine,虽然语法不同,但整体思想一致:声明式的数据流处理

✅ 分离UI逻辑与业务逻辑

为了进一步降低耦合,我们将一部分逻辑抽离成了专门的UseCase类(又称Interactor),例如:

class GetTaskListUseCase(repository: TaskRepository) {
    operator fun invoke(): Flow<List<Task>> {
        return repository.getTasks()
    }
}

这些UseCase只关心输入输出,可以轻松复用于不同的ViewModel中。


四、真实代码片段分享:从零构建一个任务详情页

四、真实代码片段分享:从零构建一个任务详情页

这里我以“任务详情页”为例,贴一些关键代码供大家参考。

1. ViewModel

class TaskDetailViewModel(
    private val getTaskByIdUseCase: GetTaskByIdUseCase,
    private val markTaskDoneUseCase: MarkTaskDoneUseCase
) : ViewModel() {

    private val _taskDetail = MutableLiveData<UIState<Task>>()
    val taskDetail: LiveData<UIState<Task>> = _taskDetail

    fun loadTask(taskId: String) {
        viewModelScope.launch {
            _taskDetail.postValue(UIState.Loading)
            try {
                val task = getTaskByIdUseCase(taskId)
                _taskDetail.postValue(UIState.Success(task))
            } catch (e: Exception) {
                _taskDetail.postValue(UIState.Error(e))
            }
        }
    }

    fun markAsDone(task: Task) {
        viewModelScope.launch {
            try {
                markTaskDoneUseCase(task.id)
                // 刷新数据
                loadTask(task.id)
            } catch (e: Exception) {
                _taskDetail.postValue(UIState.Error(e))
            }
        }
    }
}

2. Fragment中绑定UI

class TaskDetailFragment : Fragment(R.layout.fragment_task_detail) {

    private lateinit var viewModel: TaskDetailViewModel

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

        viewModel = ViewModelProvider(this).get(TaskDetailViewModel::class.java)

        observeTaskDetail()

        val taskId = args.taskId
        if (taskId.isNotEmpty()) {
            viewModel.loadTask(taskId)
        }
    }

    private fun observeTaskDetail() {
        viewModel.taskDetail.observe(viewLifecycleOwner) { state ->
            when (state) {
                is UIState.Success -> {
                    binding.progressBar.hide()
                    showTaskDetail(state.data)
                }
                is UIState.Loading -> {
                    binding.progressBar.show()
                }
                is UIState.Error -> {
                    binding.progressBar.hide()
                    Toast.makeText(requireContext(), "加载失败:${state.exception.message}", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

五、遇到的坑 & 解决之道

🐞 坑1:多个ViewModel共享状态时混乱不堪

初期我们发现同一个任务状态在多个页面展示,各自监听自己的ViewModel,结果造成了状态不一致。比如A页面改完状态,B页面毫无反应。

解决办法:引入SharedViewModel + SavedStateHandle,通过Activity层级的ViewModel来共享页面间的状态。

🐞 坑2:LiveData重复订阅导致界面闪退

当某个页面多次进入退出时,如果我们没有正确设置观察者的生命周期,会出现重复回调和内存泄漏。

解决办法:永远用observe(viewLifecycleOwner)而不是全局上下文。并在Fragment销毁时清理相关状态。

🐞 坑3:协程中抛异常没被捕获,崩溃

我们在ViewModel里调用协程时,如果没有catch住异常,会导致整个App崩溃。

解决办法:使用CoroutineExceptionHandler统一处理异常;或使用Result<T>包装返回值并显式处理错误。


六、重构后的效果与收益

这套架构上线两个月后,我们回过头来看,发现几个显著的变化:

  • 代码量反而减少了约15%,因为大量的重复逻辑被抽象成统一UseCase;
  • 新功能开发速度提升明显,因为大部分模板已经成型,只需专注业务;
  • Bug数量大幅下降,尤其是状态相关的UI问题大大减少;
  • 单元测试覆盖率提升到了70%以上,ViewModel完全隔离了逻辑;
  • iOS和Android两端的开发语言差异变小了,架构思路统一之后交流更顺畅。

更重要的是,现在新人加入项目时,基本上半天就能看懂整个页面结构。


七、我的几点建议 & 注意事项

  1. 不要盲目套用模板。MVVM是一种设计思想,不是“万能公式”。根据自己的业务规模去裁剪,别一开始就搞得太复杂。

  2. 善用工具库。Jetpack、Koin/Dagger、Coroutines、Hilt等现代库已经帮你解决了大量底层工作,不必重复造轮子。

  3. 保持一致性比追求完美更重要。架构选型定下来后,就要强制执行统一规范。哪怕一开始的设计有点糙,只要方向是对的,后面慢慢迭代就行。

  4. 做好技术文档和Code Review机制。MVVM强调分工,如果大家都按自己的方式写,迟早会失控。

  5. 注意跨平台适配。如果你同时开发安卓和iOS,可以考虑统一状态管理方式(比如Redux-like架构),提升协同效率。


八、写在最后:MVVM并不是终点

如今再回过头来看当初那段焦头烂额的日子,我很庆幸自己做了那个看似“麻烦”的选择。MVVM让我们摆脱了MVC时代的泥潭,让团队协作更加高效,也让产品迭代更加从容。

当然,我也知道未来还会面临更多的架构挑战,比如MVI、Clean Architecture、Multiplatform架构演变等等。而MVVM只是旅程中的第一步。

就像一位同事说的:“好的架构,应该是你写得越久越舒服的。” MVVM对我们来说,就是这样一个开始。

希望这篇文章能给正在学习和使用MVVM的朋友们带来一点启发和信心。如果你有任何问题,欢迎随时留言交流!


📌 如果你喜欢这类实战分享,也可以关注我后续的技术博客更新,我会继续记录团队在移动架构探索过程中的点滴成长 😊

评论 0

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