移动应用架构设计:MVVM实战 —— 从“简历焦虑”到深夜重构的血泪史

技术_宋玉_工程师
2025-12-18 02:43
阅读 1776

凌晨1点23分,办公室只剩我一个人。咖啡早就凉了,MacBook Pro 的风扇还在倔强地嗡嗡作响。窗外写字楼黑漆漆一片,只有我们这层还亮着灯——别误会,不是我在卷,是今天必须把新版本的架构方案跑通,否则明天站会上产品经理又要问:“那个加载慢得像PPT的问题,解决了吗?”

我是谁?一个刚入职两个月的移动端开发,白天写业务逻辑,晚上研究底层原理,周末刷LeetCode顺便更新简历(你懂的,互联网寒冬,不备点Plan B心里发慌)。上家公司用的是MVP + 自定义状态管理,代码写得像意大利面;现在这家,技术栈偏保守,但团队氛围不错——除了测试总在我提测后一秒发现致命Bug。

说到简历,最近在招聘软件上看到越来越多岗位要求“熟悉 MVVM 架构”、“有 Jetpack Compose / SwiftUI 实战经验”。甚至有个JD写着:“有 Springboot 后端协作经验优先”——我当时就笑了,移动开发还要会后端?但转念一想,现在全栈化趋势这么猛,说不定哪天我就要自己搭个 mock server 跑接口了。

所以,这篇文章与其说是技术分享,不如说是我被“简历焦虑”+“项目 deadline”双重逼迫下的自救记录。主题:移动应用架构设计中的 MVVM 实战。不讲理论八股,只聊真实踩坑、性能对比和深夜改代码时的心路历程。


起因:那个“卡成PPT”的首页

事情要从上周五说起。产品经理拿着竞品App截图来找我:“你看人家首页滑得跟德芙一样丝滑,咱们这个下拉刷新转半天圈,列表滑两下就卡顿,用户差评都堆成山了。”

我打开 DevTools 一看,好家伙:主线程 CPU 占用飙到90%,内存泄漏警告满屏飞。问题出在哪?旧架构是纯 MVC —— Activity 里塞了网络请求、数据解析、UI 更新、事件处理,代码超过800行。更离谱的是,每次刷新都重新创建整个列表 Adapter,RecycleView 根本扛不住。

领导拍板:“重构!用 MVVM,Jetpack 组件全家桶安排上。”
我内心OS:“说得轻巧,你倒是别在周三前加需求啊!”

但抱怨归抱怨,活还得干。既然要重构,就得搞清楚:为什么选 MVVM?它真比 MVP、MVC 香吗?


架构选型:不是所有“V”都值得信

先别急着码代码,我翻了三天 GitHub 和 Stack Overflow,又扒了几个大厂开源项目的架构文档,做了个简单对比:

架构模式 数据流 可测试性 生命周期感知 代码耦合度 学习曲线
MVC Controller → Model → View 差(View/Controller 紧耦合)
MVP Presenter 解耦 View/Model 中(Presenter 可单元测试)
MVVM ViewModel + LiveData/StateFlow 高(ViewModel 纯逻辑) 强(自动处理生命周期) 中高

结论很明显:MVVM 在可维护性和性能优化上优势突出,尤其配合 Android 的 ViewModel + LiveData(或 Kotlin Flow),能天然避免内存泄漏——因为系统会在 Activity 销毁时自动清理观察者。

但 iOS 咋办?别慌,SwiftUI 天然就是响应式 + 状态驱动,配合 ObservableObject@Published,几乎原生支持 MVVM。跨平台团队的同学甚至可以用 KMM(Kotlin Multiplatform Mobile)共享 ViewModel 逻辑——虽然目前还不太成熟,但未来可期。

至于 Springboot?别笑,还真用上了!为了快速验证接口字段变更对 UI 的影响,我用 Springboot 搭了个本地 mock 服务,5分钟搞定 CRUD 接口,省去了等后端联调的痛苦。代码长这样:

@RestController
public class MockDataController {
    @GetMapping("/api/v1/feed")
    public ResponseEntity<List<Post>> getFeed() {
        return ResponseEntity.ok(Arrays.asList(
            new Post("深夜写代码的程序员", "今天又改了10个bug..."),
            new Post("产品经理的梦想", "希望APP永远不崩")
        ));
    }
}

跑起来后,移动端直接连 http://localhost:8080,再也不用求后端兄弟临时改字段了。有时候,一个简单的 Springboot 服务,比10次站会都管用。


实战:Android 端 MVVM 重构三步走

第一步:拆分 ViewModel,告别“上帝类”

旧代码里,Activity 既管网络又管缓存还管跳转,重构第一步就是把逻辑抽到 HomeViewModel

class HomeViewModel(
    private val repository: FeedRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    init {
        loadFeed()
    }

    private fun loadFeed() {
        viewModelScope.launch {
            try {
                val feed = repository.getFeed()
                _uiState.value = UiState.Success(feed)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "未知错误")
            }
        }
    }
}

sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Post>) : UiState()
    data class Error(val message: String) : UiState()
}

注意几个细节:

  • StateFlow 替代 LiveData,因为它是 Kotlin 协程生态的一部分,支持背压、冷流等高级特性;
  • viewModelScope 自动管理协程生命周期,Activity 销毁时自动取消,再也不用手动 cancel
  • 所有状态集中管理,UI 层只需观察 uiState 并渲染对应视图。

第二步:UI 层只负责“展示”,不负责“思考”

Activity 代码清爽到让我感动:

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val viewModel: HomeViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    when (state) {
                        is UiState.Loading -> showLoading()
                        is UiState.Success -> showFeed(state.data)
                        is UiState.Error -> showError(state.message)
                    }
                }
            }
        }
    }
}

关键点:

  • 使用 repeatOnLifecycle 避免在后台收集导致的内存浪费;
  • 完全解耦业务逻辑,测试时只需 mock viewModel.uiState 即可验证 UI 行为。

第三步:Repository 层统一数据源

以前网络请求、数据库读写散落在各处,现在统一由 FeedRepository 管理:

class FeedRepository(
    private val remoteDataSource: FeedApiService,
    private val localDataSource: FeedDao
) {
    suspend fun getFeed(): List<Post> {
        // 先尝试从网络获取(带缓存策略)
        return try {
            val remote = remoteDataSource.fetchFeed()
            // 保存到本地DB
            localDataSource.insertAll(remote)
            remote
        } catch (e: Exception) {
            // 网络失败,降级到本地缓存
            localDataSource.getAll()
        }
    }
}

这种“单一可信源”模式,让数据一致性问题大幅减少。以前线上出过一次事故:网络返回新数据但没更新本地缓存,用户下拉刷新后看到旧内容,被投诉“数据不同步”。现在?一次写入,处处一致。


性能对比:真的快了吗?

重构完当然要 benchmark。我在 Pixel 4 上跑了三次冷启动 + 首页加载:

指标 旧架构 (MVC) 新架构 (MVVM)
冷启动时间 2.1s 1.7s
列表首屏渲染 1.8s 0.9s
内存占用 (稳定后) 180MB 120MB
主线程阻塞次数 12次 2次

效果立竿见影!尤其是列表滚动帧率,从平均 45 FPS 提升到 58 FPS,接近满帧。原因很简单:UI 线程不再做任何耗时操作,所有数据准备都在后台协程完成。


跨平台适配:iOS 也不甘落后

我们 App 是双端并行开发,iOS 同事用 SwiftUI + Combine 也实现了类似架构:

class HomeViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    func loadFeed() {
        isLoading = true
        FeedService.shared.getFeed { [weak self] result in
            DispatchQueue.main.async {
                self?.isLoading = false
                switch result {
                case .success(let posts):
                    self?.posts = posts
                case .failure(let error):
                    self?.errorMessage = error.localizedDescription
                }
            }
        }
    }
}

虽然语法不同,但思想一致:状态驱动 UI,逻辑与视图分离。双端甚至可以共用同一套 API 文档和状态枚举,沟通成本直降50%。


发布上线:别忘了 Google Play 的“惊喜”

重构完、测试过、Code Review 通过,终于到了发布环节。结果 Google Play Console 给我弹出警告:“你的 APK 包体积增加了 2.3MB”。

一查才发现,Jetpack 库引入了不少依赖。赶紧用 R8 混淆 + 资源压缩,又删掉无用的 drawable,最终增量控制在 800KB 以内。教训:架构升级不能只看代码优雅,还得考虑用户流量和安装转化率


结语:架构不是银弹,但值得投入

现在回头看,这次 MVVM 重构花了我整整一周的深夜时间,但换来的是:

  • Bug 率下降 60%
  • 新人接手速度快了一倍
  • 我终于敢在简历上写“精通 MVVM 架构设计”了(笑)

有人说:“小项目没必要上 MVVM,MVC 够用就行。”
但我想说:当你开始考虑“以后怎么维护”时,就已经不是小项目了

最后,给和我一样在深夜敲代码的兄弟们一句忠告:别怕重构,别怕学习新东西。毕竟,简历上的每个技术关键词,都是你熬过的夜、掉过的头发换来的勋章

哦对了,Springboot mock server 的代码我已经放到公司内部 GitLab 了,谁要用自取——前提是请我喝杯瑞幸。

评论 0

最热最新
暂无评论
技术_宋玉_工程师Lv.1
0
影响力
0
文章
0
粉丝