移动应用架构设计:MVVM实战,从踩坑到真香

周末写代码
2025-12-16 00:29
阅读 254

上周五下班前,产品小王又在群里@我:“哥,下个月初要上线新功能,UI都定稿了,前端能不能先跑起来看看?”
我瞥了一眼时间——17:58,离打卡还有两分钟。心里默默翻了个白眼,但嘴上还得回:“行,我安排。”
毕竟,在我们这家上海某低调国企,虽然不加班、双休雷打不动(这点真香),但“需求提得急、改得更急”依然是常态。

我是老张,坐标浦东,租住在公司步行十分钟的小区里,每天通勤时间比泡咖啡还短。白天写代码,晚上刷LeetCode顺便写点博客,主打一个“稳定中求进步”。最近半年被领导点名要搞清楚“现代移动架构”,理由很朴素:“咱们App代码越来越像意大利面条,再不重构,明年审计怕是要出事。”

于是,我硬着头皮啃起了 MVVM(Model-View-ViewModel)。今天这篇不是教科书式的理论科普,而是实打实的踩坑实录——包括为什么我一度想把手机扔黄浦江,以及最后怎么靠 MVVM 把项目救回来的。


一、起因:那个“区块链资源展示页”的噩梦

事情得从去年底说起。公司突然接到一个“政企合作”项目,要做一个移动端页面,展示某种区块链相关的资源流转记录。听起来高大上,实际就是个列表+详情页,数据从后端接口来,前端负责渲染。

但问题来了:

  • 数据结构嵌套深(区块高度、交易哈希、资产ID、时间戳……)
  • 需要实时轮询更新(每10秒拉一次)
  • UI 要支持深色模式 + 多语言
  • 产品经理还临时加了“点击卡片跳转外部浏览器”的需求

最开始我图快,直接用老办法:Activity 里塞一堆逻辑,网络请求、状态判断、UI 更新全混在一起。结果?测试小李三天提了27个 Bug,其中15个是“状态没刷新”或“空指针崩溃”。

有次线上用户反馈:“点进去一片空白!” 我查日志发现——哦,因为区块链节点偶尔返回 null,而我的代码压根没判空。当时真的坐在工位上扶额:这代码是我写的吗?简直是对“国企程序员”身份的侮辱!


二、为什么选 MVVM?

其实我们团队早就在讨论架构升级。隔壁组用 MVP 写了个模块,结果 Presenter 肥得像猪,测试覆盖率低得可怜。领导拍板:“试试 MVVM,听说 Google 官方推荐,还能配合 Jetpack。”

我一开始是抗拒的。心想:又是新概念?不会又是“为了架构而架构”吧?但现实逼人——再不拆分逻辑,下次发版可能真要加班了(虽然我们原则上不加班,但谁想周末被叫来修线上 Bug?)。

MVVM 的核心思想很清晰:

  • View 只负责 UI 展示和用户交互(Activity/Fragment)
  • ViewModel 承载业务逻辑,对外暴露可观察的数据
  • Model 封装数据源(网络、数据库、本地缓存)

最关键的是:View 和 ViewModel 通过 LiveData 或 StateFlow 解耦,生命周期自动管理,再也不用手动写 isFinishing() 判断了!


三、踩坑实录:那些让我半夜惊醒的瞬间

坑1:LiveData 的“粘性”事件搞崩了测试环境

我兴冲冲地把网络请求移到 ViewModel 里,用 MutableLiveData 包装结果。结果 QA 同学跑来说:“怎么每次切回 App,列表都自动刷新一次?”

查了半天才发现:LiveData 是“粘性”的!一旦有新 Observer 订阅(比如 Activity 重建),就会立刻收到上一次的值。这在详情页跳转时特别致命——用户点进详情,按 Home 键再回来,列表居然重新加载了!

解决方案:要么用 Event<T> 包装一次性事件,要么干脆上 StateFlow(Kotlin 协程生态更友好)。我们最终选了后者,配合 lifecycleScope 使用,清爽多了。

class ResourceViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<ResourceUiState>(ResourceUiState.Loading)
    val uiState: StateFlow<ResourceUiState> = _uiState.asStateFlow()

    fun loadBlockchainResources() {
        viewModelScope.launch {
            try {
                val data = repository.fetchResources()
                _uiState.value = ResourceUiState.Success(data)
            } catch (e: Exception) {
                _uiState.value = ResourceUiState.Error(e.message ?: "未知错误")
            }
        }
    }
}

坑2:多平台适配?别忘了 iOS 同事的怨念

我们 App 是跨端的(Android + iOS),但后端接口字段命名风格混乱,有的用 camelCase,有的用 snake_case。Android 用 Gson 自动解析还好说,iOS 同事天天在群里哀嚎:“你们 Android 能不能统一一下字段?”

后来我们在 Model 层加了一层 DTO(Data Transfer Object),专门做字段映射:

data class ResourceDto(
    @SerializedName("block_height") val blockHeight: Long,
    @SerializedName("tx_hash") val txHash: String,
    @SerializedName("asset_id") val assetId: String
)

// 再转成领域模型
data class BlockchainResource(
    val blockHeight: Long,
    val transactionHash: String,
    val assetId: String
)

虽然多写了几行代码,但换来 iOS 同事一句“谢了兄弟”,值了。

坑3:性能优化:别让轮询吃掉用户电量

最初实现轮询时,我直接在 ViewModel 里开了个 while(true) 协程,每10秒调一次接口。结果测试机发热严重,电量掉得飞快。

后来改成用 WorkManager + Foreground Service(仅在页面可见时轮询),并在 Activity 的 onPause 里暂停轮询:

override fun onResume() {
    super.onResume()
    viewModel.startPolling()
}

override fun onPause() {
    viewModel.stopPolling()
    super.onPause()
}

同时,加了防抖逻辑:如果上次请求还没结束,就不发起新的请求。用户体验立马稳了。


四、效果对比:重构前后 vs 心态变化

维度 重构前(MVC 混合) 重构后(MVVM)
代码行数(核心模块) ~800 行 ~600 行(逻辑更清晰)
Crash 率(上线后7天) 0.8% 0.05%
新人上手时间 3天 1天
单元测试覆盖率 12% 68%
我的心情 “这破班一天也上不下去了” “双休稳了,今晚吃火锅”

最爽的是,上周发版后,产品小王居然主动说:“这次 Bug 少多了,辛苦啦!” —— 这可是稀有物种发言!


五、给 fellow 程序员的建议

  1. 别为了 MVVM 而 MVVM:如果只是个静态展示页,没必要硬套。但凡涉及状态管理、异步操作、多端协作,MVVM 的优势就出来了。
  2. ViewModel 不是万能垃圾桶:别把所有逻辑都塞进去!复杂的业务规则应该下沉到 UseCaseRepository 层。
  3. 善用工具链:Android Studio 的 Layout Inspector + Profiler 能帮你快速定位 UI 卡顿;配合 Firebase Crashlytics,线上问题一目了然。
  4. 和 iOS 对齐规范:尤其是字段命名、错误码、状态码,早点统一,少掉头发。

结语:稳定,才是最大的生产力

在国企干了四年,我越来越觉得:代码质量 > 功能数量。领导不在乎你用了多酷的技术,但在乎“系统稳不稳定”、“审计过不过关”。MVVM 虽然学习曲线有点陡,但它带来的可维护性、可测试性,真的让我们团队从“救火队员”变成了“优雅开发”。

现在,我的 App 已经顺利上架各大应用市场,连华为应用市场的审核都一次过(感动哭)。至于那个区块链资源页?用户反馈“加载快、不闪退”,产品经理甚至拿去当标杆案例汇报了。

所以啊,别怕重构。只要思路清晰、步步为营,哪怕是在“节奏慢悠悠”的国企,也能写出让人骄傲的代码。

对了,下周我要研究 Compose + MVVM 的组合拳。如果你也在折腾,欢迎留言交流——或者,请我喝杯瑞幸也行(笑)。

评论 0

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