从产品经理转行写代码后,我终于搞懂了 MVVM —— 一个斜杠青年的移动架构实战手记
大家好,我是阿哲。两年前,我还在会议室里画着 PRD、拉着 UI 和开发扯皮“这个按钮是不是应该再大 2px”,如今却坐在工位上一边 debug 一边喝着第三杯冰美式。没错,我就是那个传说中的“前产品经理转技术”的斜杠青年。
转型的原因?说白了,被自己写的 PRD 气到了。当时给 App 做了一个看似简单的“动态表单”功能,结果上线后 Bug 多到 QA 小姐直接把我拉进群@:“你这需求逻辑能跑通算我输”。那一刻,我突然意识到:不懂代码的产品经理,就像不会游泳却天天指挥别人跳海的船长。于是咬咬牙,辞职学编程,再入职——成了自己曾经最“讨厌”的角色:客户端开发。
这两年在组里主要搞 Android 和 iOS 双端(别问,问就是小公司没得选),最近半年我们团队在重构一个核心业务模块,正好借这个机会把 MVVM 架构真正落地了一把。今天就和大家唠唠这段又哭又笑的 MVVM 实战经历,希望能给同样在架构泥潭里挣扎的兄弟们一点启发。
为什么是 MVVM?因为我们真的被 MVC 搞怕了
事情要从去年双11说起。那时候我们的 App 还是经典的“上帝 Activity”架构——一个 Fragment 里塞了 800 行逻辑,网络请求、数据解析、UI 更新全混在一起。结果那天凌晨两点,线上 Crash 率飙升,日志里全是 NullPointerException。运维大哥在群里咆哮:“谁又在主线程做 I/O?!”
我翻着那坨意大利面条代码,看着满屏的 findViewById() 和 onResponse() 嵌套回调,心里只有一个念头:这玩意儿根本没法维护。更惨的是,前端同事(对,我们现在管客户端叫“移动端前端”)想复用部分逻辑到新页面,结果发现根本抽不出干净的逻辑层——因为所有东西都绑死在 View 上了。
领导看不下去了,拍板:“重构!用 MVVM。”
我:???MVVM 是啥?ViewModel 吗?那 LiveData 呢?DataBinding 要不要上?
说实话,刚接触 MVVM 时我一度怀疑自己是不是又回到了产品经理时期——文档里全是“解耦”、“响应式”、“单向数据流”这种虚头巴脑的词。但真上手之后才发现,MVVM 的核心不是炫技,而是让代码变得“可读、可测、可维护”——而这恰恰是我当年作为产品最痛恨开发团队的一点:改个小需求动不动就要三天。
实战:从零搭建一个 MVVM 模块
我们这次重构的是“用户订单详情页”。需求看起来很简单:展示订单信息、支持取消/支付操作、实时刷新状态。但背后涉及多个后端接口(订单查询、操作回调、WebSocket 状态推送),还要兼容 Android 低版本和 iOS 的 UI 差异。
第一步:分层,别再把逻辑塞进 Activity!
MVVM 的精髓在于 Model-View-ViewModel 三层分离:
- Model:负责数据源(本地 DB、网络 API、缓存等)
- View:只管 UI 展示和用户交互(Activity/Fragment/SwiftUI View)
- Assistant: ViewModel:承上启下,处理业务逻辑,暴露可观察的数据给 View
我一开始犯了个经典错误:在 ViewModel 里直接调用 Retrofit 接口。后来被隔壁老王(我们组的架构师)骂了一顿:“你这是把 Model 和 ViewModel 耦合了!万一哪天要换 gRPC 或者加缓存策略,你是不是要把 ViewModel 全重写?”
于是赶紧抽离出一个 OrderRepository 作为 Model 层:
// OrderRepository.kt
class OrderRepository {
private val apiService = ApiService.create()
private val database = AppDatabase.getInstance()
suspend fun fetchOrder(orderId: String): Result<Order> {
// 先查本地缓存(比如 Room)
database.orderDao().getOrder(orderId)?.let { return Result.success(it) }
// 再走网络
return try {
val remoteOrder = apiService.getOrder(orderId)
database.orderDao().insert(remoteOrder) // 写入缓存
Result.success(remoteOrder)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun cancelOrder(orderId: String): Result<Unit> {
return apiService.cancelOrder(orderId)
}
}
看到没?Model 层完全不知道谁在调用它,也不关心 UI 长什么样。以后换后端协议?只要改这里就行。
第二步:ViewModel 别干脏活,只做“协调员”
接下来是 ViewModel。它的职责很明确:接收 View 的事件,调用 Model,然后把结果转换成 View 能理解的状态。
// OrderDetailViewModel.kt
class OrderDetailViewModel(
private val repository: OrderRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()
fun loadOrder(orderId: String) {
viewModelScope.launch {
_uiState.value = OrderUiState.Loading
when (val result = repository.fetchOrder(orderId)) {
is Result.Success -> _uiState.value = OrderUiState.Success(result.data)
is Result.Failure -> _uiState.value = OrderUiState.Error(result.exception.message ?: "加载失败")
}
}
}
fun onCancelClicked() {
// 注意:这里不直接更新 UI,而是触发一个操作
viewModelScope.launch {
repository.cancelOrder(_uiState.value.orderId)
// 成功后自动刷新(因为取消后状态会变)
loadOrder(_uiState.value.orderId)
}
}
}
sealed class OrderUiState {
object Loading : OrderUiState()
data class Success(val order: Order) : OrderUiState()
data class Error(val message: String) : OrderUiState()
}
关键点来了:
- 所有状态变更都通过
StateFlow暴露,View 只需监听这个流即可 - View 不再持有任何业务逻辑,连“怎么显示错误”都不用管,ViewModel 告诉它“现在是 Error 状态,附带错误文案”
- 操作(如取消订单)也封装成方法,View 只需调用
viewModel.onCancelClicked(),不用知道背后调了哪个后端接口
第三步:View 只做两件事:展示 & 转发
最后是 View 层(以 Android 为例):
// OrderDetailFragment.kt
class OrderDetailFragment : Fragment() {
private lateinit var binding: FragmentOrderDetailBinding
private lateinit var viewModel: OrderDetailViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentOrderDetailBinding.bind(view)
viewModel = ViewModelProvider(this)[OrderDetailViewModel::class.java]
// 订阅状态
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is OrderUiState.Loading -> showLoading()
is OrderUiState.Success -> renderOrder(state.order)
is OrderUiState.Error -> showError(state.message)
}
}
}
}
// 转发用户操作
binding.btnCancel.setOnClickListener { viewModel.onCancelClicked() }
}
}
你看,View 里没有一行业务逻辑,连“取消订单要不要二次确认”这种判断都被挪到 ViewModel 里了(通过发射一个 ConfirmDialogEvent 流)。这样一来,测试覆盖率蹭蹭涨——ViewModel 可以脱离 Android 环境单元测试,View 只需验证 UI 映射是否正确。
踩过的坑:那些让我想砸电脑的时刻
当然,实战哪有这么顺利。分享几个血泪教训:
LiveData vs StateFlow 的纠结
刚开始用 LiveData,结果发现它不支持协程挂起函数,还得套liveData{}builder。后来全面迁移到 StateFlow + Kotlin 协程,流畅多了。建议新项目直接上 StateFlow。iOS 怎么办?
我们 iOS 同事用的是 SwiftUI + Combine,虽然语法不同,但 MVVM 思想一致。关键是两端约定好状态模型(比如都叫OrderUiState),这样后端接口和业务逻辑就能高度复用。我们甚至把OrderRepository的逻辑用 KMM(Kotlin Multiplatform)抽出来共用了 70%!内存泄漏警告
别忘了在 ViewModel 里用viewModelScope,否则协程可能持有 Activity 引用。我们曾因为一个忘记 cancel 的 WebSocket 监听,导致整个页面无法回收,OOM Crash 直接上生产。过度设计陷阱
有次我给一个只有两个字段的简单页面也搞了全套 MVVM,被 leader 笑话:“你这是用火箭炮打蚊子”。记住:架构是为复杂度服务的,简单页面用 MVP 甚至 MVC 都行。
效果如何?上线后我终于能睡整觉了
重构后的模块上线三个月,Crash 率下降 60%,QA 回归测试时间缩短一半。最爽的是上周五晚上,产品临时要求加个“订单分享”按钮——我只改了 ViewModel 暴露一个新的 shareUrl 字段,View 层加一行绑定代码,搞定。全程不到 20 分钟,而以前可能要翻半天回调地狱。
顺便贴个性能对比(基于 Firebase Performance Monitoring):
| 指标 | 旧架构 (MVC) | 新架构 (MVVM) |
|---|---|---|
| 页面冷启动 | 1250ms | 890ms |
| 内存占用 | 45MB | 38MB |
| 平均 Crash 率 | 0.8% | 0.3% |
开发心得:从产品到程序员的思维转变
作为前产品经理,我现在特别理解为什么当初开发总怼我“需求不清晰”。好的架构不是为了炫技,而是为了应对变化。MVVM 的本质,是把“不确定性”(比如 UI 变动、后端接口调整)隔离在最小范围内。
如果你也在考虑引入 MVVM,我的建议是:
- 从小模块开始试水,别一上来就重构整个 App
- 状态驱动 UI,而不是反过来
- 别怕抽象,Repository、UseCase 这些层多写几行代码,后期省十倍时间
- 和后端同学对齐状态语义,比如“订单状态=已取消”到底由谁计算?避免前后端各自维护一套状态机
最后说句掏心窝子的话:从画原型到写代码,我最大的收获不是技术,而是对“可维护性”的敬畏。毕竟,我们写的不是一次性脚本,而是未来几个月甚至几年都要和它共处的系统。
好了,咖啡喝完了,该去修下一个 Bug 了。希望这篇带点人味儿的文章对你有用。如果你们也在搞 MVVM,欢迎留言交流——尤其是 iOS 那边的兄弟,求分享 SwiftUI 最佳实践!
(完)

评论 0