移动应用架构设计:MVVM实战,我的一次真实项目重构之旅
背景介绍:为什么是这次重构让我决定写这篇总结?

我在一家中型互联网公司负责Android端的产品开发,团队大概有10人左右。之前我们维护的一个用户量比较大的App(主要是社交+内容推荐场景)在迭代了几轮之后,逐渐变得臃肿、难以维护。
最开始采用的是传统的MVC架构,随着页面数量和业务复杂度增加,问题开始频繁暴露出来:
- Activity/Fragment 中逻辑越来越多,越来越难看懂
- 页面状态不好管理,经常因为数据加载顺序混乱导致UI显示异常
- 测试代码几乎无法编写,UI层依赖太重
- 多人协作时冲突频发,改一个小功能可能要牵一发动全身
去年年底,我们决定做一次“手术级”的架构升级,目标明确:提升代码可维护性、解耦视图与业务逻辑、支持更好的测试能力。于是我们选择了 MVVM(Model-View-ViewModel)架构,并搭配Jetpack组件库来实现。
这篇文章就是基于那次重构实践写的,希望能给正在走相似技术路线的开发者一些参考。
问题描述:痛点太多,不改不行了

当时我们的首页模块已经变得非常脆弱,随便加个功能就有不小的概率触发隐藏的问题。比如,当我们需要在首页增加一个"热搜榜单"模块时,遇到了几个典型的典型问题:
- 首页Activity职责过多:从网络请求、数据解析到事件处理,几乎所有活都堆在一个类里,导致这个文件超过3000行。
- 状态同步困难:多个异步任务完成后才允许更新UI,但实际执行顺序经常混乱,导致UI展示错乱或崩溃。
- 界面测试无法进行:Activity直接调用各种Service和Repository,无法Mock对象,单元测试形同虚设。
- 多人协作成本高:两个同事同时修改首页的不同模块,却总是互相影响。
当时每天开会都在讨论:“能不能拆一下?换一种结构?”直到后来我们引入MVVM后,整个开发体验一下子打开了新世界。

解决方案: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的时候,遇到了一些市场审核的问题,比如:
- 隐私合规项检查:涉及权限申请、隐私协议更新
- 首次冷启动耗时较长:由于首次运行大量初始化操作,需要合理安排流程
- 低端机型兼容性测试不够充分:建议搭建自动化真机测试环境
最后想说的话:架构不是终点,而是起点

说实话,我以前总认为写功能才是核心,架构什么的都是形式主义。但经历过这次重构后,我才真正体会到“工欲善其事,必先利其器”的道理。
MVVM 并不是什么新鲜玩意儿,但它确实帮我们解决了一个又一个现实中的难题。更重要的是,它让我们重新思考了如何更好地组织代码、如何协作、如何让系统具备更强的扩展性和可维护性。
如果你现在正面临类似的困境,不妨勇敢迈出重构的第一步。你可以慢慢来,一个模块一个模块地改,不用着急。只要你愿意去尝试,一定会看到不一样的结果。
最后,送大家一句话共勉:“好的代码结构,是未来一切可能性的基础。”
作者:@老张,在一线互联网公司搬砖多年,热爱写代码,也喜欢写文章记录点滴成长。欢迎关注我的GitHub或掘金账号交流更多移动端开发实战经验。

评论 0