移动应用架构设计:MVVM实战,我的一次真实项目重构之旅

云原生笔记本
2025-06-14 03:02
阅读 219

背景介绍:为什么是这次重构让我决定写这篇总结?

背景介绍:为什么是这次重构让我决定写这篇总结?

我在一家中型互联网公司负责Android端的产品开发,团队大概有10人左右。之前我们维护的一个用户量比较大的App(主要是社交+内容推荐场景)在迭代了几轮之后,逐渐变得臃肿、难以维护。

最开始采用的是传统的MVC架构,随着页面数量和业务复杂度增加,问题开始频繁暴露出来:

  • Activity/Fragment 中逻辑越来越多,越来越难看懂
  • 页面状态不好管理,经常因为数据加载顺序混乱导致UI显示异常
  • 测试代码几乎无法编写,UI层依赖太重
  • 多人协作时冲突频发,改一个小功能可能要牵一发动全身

去年年底,我们决定做一次“手术级”的架构升级,目标明确:提升代码可维护性、解耦视图与业务逻辑、支持更好的测试能力。于是我们选择了 MVVM(Model-View-ViewModel)架构,并搭配Jetpack组件库来实现。

这篇文章就是基于那次重构实践写的,希望能给正在走相似技术路线的开发者一些参考。


问题描述:痛点太多,不改不行了

问题描述:痛点太多,不改不行了

当时我们的首页模块已经变得非常脆弱,随便加个功能就有不小的概率触发隐藏的问题。比如,当我们需要在首页增加一个"热搜榜单"模块时,遇到了几个典型的典型问题:

  1. 首页Activity职责过多:从网络请求、数据解析到事件处理,几乎所有活都堆在一个类里,导致这个文件超过3000行。
  2. 状态同步困难:多个异步任务完成后才允许更新UI,但实际执行顺序经常混乱,导致UI展示错乱或崩溃。
  3. 界面测试无法进行:Activity直接调用各种Service和Repository,无法Mock对象,单元测试形同虚设。
  4. 多人协作成本高:两个同事同时修改首页的不同模块,却总是互相影响。

当时每天开会都在讨论:“能不能拆一下?换一种结构?”直到后来我们引入MVVM后,整个开发体验一下子打开了新世界。

移动设备适配-2


解决方案:MVVM + Jetpack组件,轻装上阵

解决方案:MVVM + Jetpack组件,轻装上阵

我们选择使用 Android 官方推荐的 MVVM 架构,结合 Jetpack 组件(如 ViewModel、LiveData、Room 等),将原有的 MVC 结构彻底翻新。

架构分层说明:

层级 作用说明
View 主要是 Fragment 和 Activity,只负责 UI 渲染和用户交互
ViewModel 持有页面数据和逻辑,通过 LiveData 向 View 暴露数据变化
Repository 封装数据获取逻辑,统一对外接口(本地+远程)
Model 数据模型(Entity 或 POJO)

这样做的好处非常明显:

  • View 和 Model 完全分离,便于单元测试
  • ViewModel 生命周期感知,不会因为屏幕旋转而丢失数据
  • LiveData 自动观察生命周期,避免内存泄漏

代码实践:重构后的首页结构长什么样?

以首页热搜榜单模块为例,来看看我们在MVVM下的具体实现方式。

1. 数据模型定义(Model)

data class HotSearchItem(
    val id: String,
    val title: String,
    val coverUrl: String,
    val rank: Int,
    val hotLevel: Int
)

2. Repository层定义数据源逻辑

class HomeRepository private constructor() {
    companion object {
        val instance = HomeRepository()
    }

    // 真实项目中会封装远程和本地的数据获取逻辑
    fun fetchHotSearchList(): MutableLiveData<List<HotSearchItem>> {
        val liveData = MutableLiveData<List<HotSearchItem>>()
        // 模拟网络请求
        CoroutineScope(Dispatchers.IO).launch {
            delay(500)
            val list = mockRemoteData()
            withContext(Dispatchers.Main) {
                liveData.value = list
            }
        }
        return liveData
    }
}

3. ViewModel层承载业务逻辑

class HomePageViewModel : ViewModel() {
    private val hotSearchListLiveData = MutableLiveData<List<HotSearchItem>>()

    fun loadHotSearchList() {
        HomeRepository.instance.fetchHotSearchList().observeForever { list ->
            hotSearchListLiveData.value = list
        }
    }

    fun getHotSearchList(): LiveData<List<HotSearchItem>> {
        return hotSearchListLiveData
    }
}

4. View层只处理UI渲染

class HomeFragment : Fragment() {

    private lateinit var viewModel: HomePageViewModel
    private lateinit var adapter: HotSearchListAdapter

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_home, container, false)
        initView(view)
        return view
    }

    private fun initView(view: View) {
        viewModel = ViewModelProvider(this)[HomePageViewModel::class.java]

        adapter = HotSearchListAdapter()
        val recyclerView = view.findViewById<RecyclerView>(R.id.hot_search_recycler)
        recyclerView.adapter = adapter

        viewModel.getHotSearchList().observe(viewLifecycleOwner, Observer { list ->
            adapter.submitList(list)
        })

        viewModel.loadHotSearchList()
    }
}

这样拆分后,Fragment 的职责清晰了许多。ViewModel 可以单独做逻辑校验,甚至可以直接 Mock 出数据供测试使用。


踩坑经验:你以为很简单,其实有很多细节需要注意

虽然看起来结构很清晰,但在实际落地过程中还是踩了不少坑:

1. ViewModel 的范围容易搞混

刚开始大家都把 ViewModel 放在 Activity 里面,结果同一个 Activity 内的多个 Fragment 共享了 ViewModel,导致数据互相干扰。

解决方法

  • 如果希望每个 Fragment 使用独立的 ViewModel,使用 ViewModelProvider(this)
  • 如果希望共享,则可以传入宿主 Activity:ViewModelProvider(requireActivity())

2. LiveData 不适合传递事件类型的数据

例如弹窗、跳转等一次性事件,如果直接通过 LiveData 发送,可能会被多次消费(特别是在页面重建时)。

解决方案

使用 SingleLiveEvent 或者自定义一个仅消费一次的 Event 封装类。

class SingleLiveEvent<T> : MutableLiveData<T>() {
    private val pending = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        if (hasActiveObservers()) {
            // 已经有一个观察者了,说明重复监听了
            Log.w("SingleLiveEvent", "Multiple observers registered but only one will be notified of changes.")
        }

        super.observe(owner) { t ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        }
    }

    override fun setValue(t: T?) {
        pending.set(true)
        super.setValue(t)
    }
}

3. 单元测试写起来没那么顺手

一开始写完 ViewModel 很兴奋以为能马上测起来,但发现 Repository 是单例类,而且没有抽象接口,mock 非常麻烦。

后来调整策略

  • 将 Repository 抽象为接口,注入到 ViewModel 中
  • 使用 Dagger/Hilt 进行依赖注入
  • 单元测试中替换为 mock 实现

这一步虽然增加了初始工作量,但后续收益巨大,大大提高了可测试性。


效果总结:重构带来的实际变化

经过三个月的逐步迁移,我们完成了 App 核心模块的架构升级。以下是几个关键指标的变化:

维度 重构前 重构后
单模块平均代码量 2500+ 行 ~1200 行
新人熟悉成本 至少 2~3 周 1周以内
单元测试覆盖率 < 5% > 40%
接口变更影响面 常常波及多个页面 影响集中在 Repository 层
异常排查效率 需要反复调试、打日志 更容易定位问题源头
多人协同 冲突频繁,需靠人力规避 分层清晰,冲突大幅减少

更重要的是,团队成员普遍反馈开发节奏更顺畅了,心情也更好了 😄


我的经验分享:给移动开发者的几点建议

✅ 1. 要舍得花时间做架构设计

很多人觉得赶进度来不及做架构,但实际上前期不打好基础,后期的成本只会越来越高。就像你盖楼,地基不稳,后面每多一层压力越大。

我的建议是:一开始就规划好分层架构,哪怕先小步尝试,也要比无脑往上加功能强得多。

✅ 2. MVVM不是万能的,也不是唯一的答案

MVVM 在 Android 上非常适合,特别是配合 Jetpack,官方支持也很到位。不过它更适合中大型项目,对于特别简单的页面,也可以灵活处理。

如果你还在用 MVC,建议从小模块入手试点,不要一口吃成胖子。

✅ 3. 注意适配不同手机品牌和系统版本

尤其是在国内安卓生态复杂的情况下,一定要注意机型兼容性和系统差异。比如某些老设备不能正确处理 LiveData,或者存在内存回收问题。

我们曾经遇到某个低端机在横竖屏切换时频繁重建 ViewModel,后来通过手动保存恢复数据解决了。

✅ 4. 用户体验和性能不能忽视

再好的架构也是为了支撑最终的产品体验服务。我们在重构期间没有放松对性能的要求,反而加强了以下几方面:

  • 冷启动优化:控制ViewModel初始化时机,避免阻塞主线程
  • 数据懒加载:有些页面不真正访问就不请求数据
  • UI更新优化:避免无效刷新,合理使用DiffUtil

这些优化和架构本身并不冲突,只要设计得当,两者完全可以兼得。

✅ 5. 发布到各大应用市场的注意事项

虽然不是架构层面的事情,但也值得一提。我们在上线新版App的时候,遇到了一些市场审核的问题,比如:

  • 隐私合规项检查:涉及权限申请、隐私协议更新
  • 首次冷启动耗时较长:由于首次运行大量初始化操作,需要合理安排流程
  • 低端机型兼容性测试不够充分:建议搭建自动化真机测试环境

最后想说的话:架构不是终点,而是起点

移动设备适配-1

说实话,我以前总认为写功能才是核心,架构什么的都是形式主义。但经历过这次重构后,我才真正体会到“工欲善其事,必先利其器”的道理。

MVVM 并不是什么新鲜玩意儿,但它确实帮我们解决了一个又一个现实中的难题。更重要的是,它让我们重新思考了如何更好地组织代码、如何协作、如何让系统具备更强的扩展性和可维护性。

如果你现在正面临类似的困境,不妨勇敢迈出重构的第一步。你可以慢慢来,一个模块一个模块地改,不用着急。只要你愿意去尝试,一定会看到不一样的结果。

最后,送大家一句话共勉:“好的代码结构,是未来一切可能性的基础。”


作者:@老张,在一线互联网公司搬砖多年,热爱写代码,也喜欢写文章记录点滴成长。欢迎关注我的GitHub或掘金账号交流更多移动端开发实战经验。

评论 0

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