MVVM实战:一个全栈工程师的移动架构进阶之路

@高建军
2025-06-16 17:57
阅读 684

作为一名从事移动端开发多年的全栈开发者,我经历过从MVC到MVVM的过渡过程,也亲历了架构演变对项目长期稳定性和团队协作效率带来的深远影响。今天我想和你聊聊我在一个中型电商类APP重构过程中如何采用MVVM架构解决痛点,并在此过程中积累的一些实战经验和踩过的坑。


一、背景与挑战:为什么我们不得不重构?

一、背景与挑战:为什么我们不得不重构?

故事要从两年前讲起。当时我们团队负责维护一款用户量约200万的电商平台App。初期为了快速上线,我们采用了Android原生开发+传统MVP模式,虽然前期开发速度快,但随着功能模块日益增多,团队人数扩大到15人,问题逐渐暴露出来:

  • 业务逻辑紧耦合:很多Presenter层直接操作View,导致UI组件臃肿、复用困难
  • 数据状态不一致:多个页面都需要显示用户登录信息,一处更新另一处经常不刷新
  • 测试难度大:UI逻辑混杂在Activity里,单元测试几乎不可行
  • 多人协作冲突频发:同一个Fragment经常多人修改,Git冲突频繁

最典型的一个例子是首页商品推荐模块。该模块需要根据用户画像请求不同的接口,还要结合本地缓存、广告位轮播、点击跳转等多种交互。后来这个Fragment的代码竟然达到了800多行,每次改动都像拆定时炸弹,稍有不慎就牵一发动全身。

当时我们意识到,如果不从架构层面入手解决这些问题,后续迭代将越来越艰难。


二、选型与决策:为什么是MVVM?

移动应用界面设计-1

二、选型与决策:为什么是MVVM?

我们调研了几种主流架构方案:

架构 优点 缺点 是否适合我们
MVP 分离清晰,利于测试 Presenter膨胀快 曾经尝试过,但已遇到瓶颈
Clean Architecture 高内聚低耦合,可扩展性强 结构复杂,学习成本高 团队规模偏小,短期见效慢
MVVM + Jetpack 数据绑定友好,生命周期自动管理 初期需要投入培训成本 恰好符合我们向Jetpack靠拢的技术路线

最终我们选择了 “MVVM + Android Jetpack” 的组合拳。选择的核心理由有几个:

  1. LiveData自动感知生命周期,能有效避免内存泄漏和空指针问题
  2. ViewModel提供独立于UI的数据存储能力,提升复用性
  3. DataBinding(或现在更推荐ViewBinding)减少模板代码
  4. 配合Room进行本地持久化更加顺畅

其实当初还有一个争议点是是否引入Kotlin协程和Flow,但我们评估后认为这是未来趋势,决定在本次重构中同步推进语言升级和架构改造。


三、重构实战:如何一步步落地MVVM

1. 模块划分与职责明确

我们将整个App划分为以下几个层次:

app/
├── ui/              -> UI展示层(Fragment, Activity)
├── viewmodel/       -> ViewModel层
├── repository/      -> 数据仓库层(整合远程与本地数据源)
├── model/           -> 数据模型
├── datasource/      -> 网络请求和数据库操作
└── utils/           -> 公共工具类

以“我的订单”页面为例,重构前后结构变化如下:

原始结构(紧耦合)

public class MyOrderActivity extends AppCompatActivity {
    private List<Order> orders = new ArrayList<>();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        apiService.getOrders(userId).enqueue(new Callback<List<Order>>() {
            @Override
            public void onResponse(...) {
                orders.clear();
                orders.addAll(body());
                adapter.notifyDataSetChanged();
            }
        });
    }
}

移动应用界面设计-2

改造后MVVM结构

UI层(MyOrderFragment.kt)

class MyOrderFragment : Fragment() {

    private val viewModel by viewModels<MyOrderViewModel>()

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

        // 观察数据变化并更新UI
        viewModel.orderList.observe(viewLifecycleOwner) { list ->
            orderAdapter.submitList(list)
        }

        viewModel.loadOrders()
    }
}

ViewModel层(MyOrderViewModel.kt)

class MyOrderViewModel(
    private val orderRepository: OrderRepository
): ViewModel() {

    val orderList = MutableLiveData<List<Order>>()

    fun loadOrders() {
        viewModelScope.launch {
            val result = orderRepository.fetchOrdersFromNetworkOrCache()
            orderList.postValue(result)
        }
    }
}

Repository层(OrderRepository.kt)

class OrderRepository(
    private val apiService: OrderApi,
    private val localDataSource: OrderLocalDataSource
) {
    suspend fun fetchOrdersFromNetworkOrCache(): List<Order> {
        // 先查本地缓存
        val cached = localDataSource.getCachedOrders()
        if (cached.isNotEmpty()) return cached
        
        // 如果没有再走网络拉取
        val network = apiService.getOrders()
        localDataSource.saveToCache(network)
        return network
    }
}

通过这样分层解耦,不仅让每个层级职责单一,还方便我们在后期接入Mock数据、切换数据源甚至迁移后台服务。


2. 生命周期与数据一致性保障

Jetpack ViewModel的一大好处是它可以跨越配置变更存活。例如,当手机旋转屏幕时,传统的onCreate会重新加载数据,而使用ViewModel则可以保留当前状态。

举个实际场景:

假设用户正在查看一个长列表的商品详情页,突然旋转屏幕。如果数据加载没有妥善处理,很可能出现短暂空白或者重复请求API。

解决方案是我们使用 SavedStateHandle 来保存一些基础状态:

class ProductDetailViewModel(
    savedStateHandle: SavedStateHandle,
    repository: ProductRepository
): ViewModel() {

    val productId = savedStateHandle.get<String>("product_id") ?: ""

    init {
        loadProductDetail(productId)
    }
}

同时,在Fragment中传递参数的时候使用 setArguments(),并通过 by navGraphViewModels() 管理共享状态,大大提升了用户体验的一致性。


3. 协程与异步任务优化

我们全面替换了旧有的RxJava为Kotlin Coroutines + Flow。这一转变带来了两大好处:

  1. 写法更自然,不需要复杂的调度器嵌套
  2. 上下文管理更安全,viewModelScope会自动取消未完成的协程,防止内存泄露

例如一个简单的登录流程:

class LoginViewModel: ViewModel() {

    val loginResult = MutableLiveData<Boolean>()

    fun login(username: String, password: String) {
        viewModelScope.launch {
            try {
                val result = api.login(username, password)
                loginResult.value = true
            } catch (e: Exception) {
                loginResult.value = false
            }
        }
    }
}

另外,我们利用SharedFlow实现了全局的消息总线,用于处理Toast提示、登录状态广播等轻量级跨页面通信需求。


4. 多平台适配与性能优化

在重构的同时,我们也着手优化了一些常见性能问题:

  • 懒加载机制:利用ViewPager2 + Fragment懒加载策略,只有真正进入可视区域才加载数据
  • 图片加载优化:统一采用Glide + RecyclerView预加载策略,减少白屏时间
  • 启动时间压缩:将非核心初始化操作放至IdleHandler或后台线程
  • 机型适配:通过BuildConfig判断设备密度、系统版本,动态调整资源路径和动画帧率

值得一提的是,我们在部分低端机上发现某些LiveData频繁触发导致页面卡顿,后来发现是因为监听过多且缺少合并通知的机制。于是我们封装了一个ThrottledLiveData来限制单位时间内事件的触发次数,解决了这个问题。


5. 发布前的收尾工作

重构完成后,我们还需要做一系列收尾动作:

  • Code Review机制:强制所有PR必须由至少一人Review,重点检查ViewModel调用是否正确、是否滥用MutableLiveData等
  • 自动化测试覆盖:针对核心ViewModel编写UnitTest,确保数据流逻辑正确
  • CI/CD集成:在构建流水线中加入代码质量扫描(Detekt)、测试覆盖率检测(Jacoco)
  • 发布灰度机制:先开放5%用户访问新架构页面,监控ANR、Crash情况

四、重构后的收益与反思

经过为期两个月的集中重构,我们的App稳定性有了显著提升:

维度 重构前 重构后
ANR频率 每天10次以上 每天0~2次
Crash率 1.2% <0.5%
平均响应速度 1.8s 1.2s
新人上手周期 4周左右 2周以内

此外,我们在团队内部形成了统一的编码风格,例如规定所有ViewModel必须通过DI注入,禁止在UI层直接操作Model等等。这些规范极大地提高了项目的可维护性。


五、给开发者的几点建议

如果你也在考虑使用MVVM架构,这里是一些实战经验分享:

  1. 不要急于求成:可以先从某个模块开始试点,逐步推广。不要一开始就全盘重写。
  2. 做好技术选型对比:Jetpack虽好,但在React Native或Flutter项目中也要权衡是否适用
  3. 注重数据驱动设计:ViewModel尽量对外暴露LiveData,而不是暴露方法让View去主动获取
  4. 警惕ViewModel滥用:不是所有状态都需要放到ViewModel里,有些临时性的界面状态应该留在View内部管理
  5. 重视架构教育:即使是高级工程师,也需要一起学习和制定最佳实践,否则很容易回到老路
  6. 持续监控与反馈:上线后务必接入性能监控SDK,及时修复问题

六、后记:技术永远服务于业务

写到这里,我不禁想起某次上线夜的情景。那天我们发布了第一个完全基于MVVM的新页面——“购物车”。凌晨三点,看着监控图表上的成功率稳步上升,心里莫名踏实了很多。那一刻我才意识到,好的架构不只是写得漂亮的代码,更是支撑起产品走向成熟的技术基石。

如今,我们已经将整个App迁移到了MVVM架构上,并在Jetpack Compose的探索道路上迈出了一小步。希望这篇文章能为你带来一点启发,哪怕只是少走一个小弯路也好。

架构这条路,我们一起前行吧 🚀

评论 0

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