别再写“上帝 Activity”了!一文讲透 Android 官方推荐的 MVVM 架构
把所有代码都写在 Activity / Fragment 里会怎样一个文件几千行、网络请求和界面逻辑搅在一起、屏幕一旋转数据就丢、想加个单元测试无从下手……这就是所谓的上帝 Activity。MVVM是 Google 官方推荐的应用架构它通过分层把界面、状态、数据清晰地隔开让代码可维护、可测试、抗配置变更。本文讲清楚 MVVM 到底是什么、每层做什么、以及它和官方架构指南的关系。一、为什么需要架构从上帝 Activity说起新手最常见的写法是把一切塞进 ActivityclassUserActivity:AppCompatActivity(){overridefunonCreate(s:Bundle?){// 网络请求、JSON 解析、数据库读写、界面更新……全在这thread{valjsonURL(api).readText()valuserparse(json)runOnUiThread{textView.textuser.name}}}}问题在哪职责混乱UI、业务逻辑、数据访问全耦合在一起抗不住配置变更屏幕一旋转 Activity 重建请求重来、数据丢失没法测试业务逻辑绑死在 Activity 上无法脱离 Android 框架单测难以维护文件越来越大改一处牵一发动全身。架构的本质通过分离关注点Separation of Concerns让每一块代码只干一件事。MVVM 就是一种成熟的分离方案。二、MVVM 的三层结构MVVM Model–View–ViewModel。核心思想是把界面从业务和数据中解耦层是谁负责什么不该做什么ViewActivity / Fragment / Composable显示界面、观察状态、把用户操作上报给 ViewModel不写业务逻辑、不直接访问数据ViewModelandroidx.lifecycle.ViewModel持有并暴露 UI 状态、处理界面逻辑、调用数据层不持有 View 引用、不碰 Android UI 类ModelRepository 数据源提供数据网络、数据库、缓存不关心界面关键的单向关系View 知道 ViewModelViewModel 不知道 View。ViewModel 只暴露状态谁来观察它都行View观察状态变化响应式而不是被动等 ViewModel “推”这种单向依赖让 ViewModel 完全独立于界面可以被单独测试。一句话ViewModel 不能持有Activity/View/ContextApplication Context 例外的引用否则配置变更时会内存泄漏也破坏了可测试性。三、ViewModel为什么它能扛住屏幕旋转ViewModel是 Jetpack 提供的类它最大的特点是生命周期比 Activity/Fragment 更长屏幕旋转、语言切换等配置变更导致 Activity 重建时ViewModel 实例会被保留数据不丢只有当 Activity真正结束用户按返回退出、finish()时ViewModel 才会收到onCleared()并销毁。classUserViewModel:ViewModel(){privateval_uiStateMutableStateFlowUiState(UiState.Loading)valuiState:StateFlowUiState_uiState.asStateFlow()funloadUser(id:Int){viewModelScope.launch{// 协程作用域随 ViewModel 自动取消_uiState.valueUiState.Loading runCatching{repository.getUser(id)}.onSuccess{_uiState.valueUiState.Success(it)}.onFailure{_uiState.valueUiState.Error(it.message)}}}overridefunonCleared(){// ViewModel 销毁时回调// 清理资源viewModelScope 会自动取消无需手动}}配合协程viewModelScope启动的协程会在 ViewModel 销毁时自动取消完美契合。四、UI 状态MVVM 的现代实践强调UI State界面状态的概念用一个不可变的数据对象完整描述界面当前该显示什么。ViewModel 暴露它View 观察它。常见做法是用密封类/接口表达互斥状态sealedinterfaceUiState{dataobjectLoading:UiStatedataclassSuccess(valuser:User):UiStatedataclassError(valmessage:String?):UiState}或用一个 data class 聚合所有界面字段dataclassUserUiState(valisLoading:Booleanfalse,valuser:User?null,valerrorMessage:String?null)View 只需照着状态渲染// ComposeComposablefunUserScreen(viewModel:UserViewModel){valstatebyviewModel.uiState.collectAsStateWithLifecycle()when(state){isUiState.Loading-LoadingSpinner()isUiState.Success-UserDetail((stateasUiState.Success).user)isUiState.Error-ErrorView((stateasUiState.Error).message)}}单一数据源Single Source of Truth界面该显示什么只由 ViewModel 暴露的状态决定。View 不再自己维护一堆零散的isLoading、data变量而是渲染同一个状态对象。这让界面行为可预测、易调试。五、Repository数据层的统一入口ViewModel 不应直接调用 Retrofit 或 Room而是通过Repository仓库访问数据。Repository 是数据层的门面负责整合多个数据源网络 API 本地数据库 缓存决定数据从哪来如先读缓存再请求网络更新向上层暴露干净的数据接口屏蔽底层细节。classUserRepository(privatevalapi:UserApi,privatevaldao:UserDao){suspendfungetUser(id:Int):User{// 例先尝试本地没有再请求网络并缓存returndao.find(id)?:api.fetchUser(id).also{dao.insert(it)}}}好处ViewModel 不关心数据从网络还是数据库来只管要一个 User。将来换数据源、加缓存策略只改 Repository上层无感知。六、官方架构指南MVVM 的全景图Google 的应用架构指南推荐把应用分为两到三层MVVM 正好落在这套分层里UI 层渲染界面 持有状态View ViewModel领域层可选当业务逻辑复杂或被多个 ViewModel 复用时抽出UseCase简单项目可省略数据层Repository 各数据源是数据的权威来源。依赖方向是单向向下UI 依赖数据层数据层不依赖 UI。MVVM 与官方架构的关系官方架构指南是更完整的工程蓝图而 MVVM 主要描述了其中UI 层View 与 ViewModel 的关系。实践中我们说用 MVVM通常就是指ViewModel 状态 Repository这一整套。七、依赖注入让各层松耦合地组装起来ViewModel 需要 RepositoryRepository 需要 Api 和 Dao……这些依赖如果手动new会到处硬编码、难以替换和测试。依赖注入DI把创建依赖和使用依赖分开。Android 官方方案是HiltHiltViewModelclassUserViewModelInjectconstructor(privatevalrepository:UserRepository// 自动注入):ViewModel(){/* ... */}测试时可以注入一个假的Repository脱离真实网络验证 ViewModel 逻辑——这正是分层 DI 带来的可测试性。Hilt 细节可单独成篇这里只点出它在架构中的位置负责把各层装配起来。八、MVVM 与 MVC、MVP 的区别模式界面与逻辑的关系主要问题 / 特点MVCController 处理逻辑但在 Android 里 Activity 既是 View 又是 Controller职责不清容易变成上帝类MVPPresenter 持有 View 接口双向调用解耦了但 Presenter 持有 View 引用需手动管理生命周期、写大量接口MVVMViewModel不持有ViewView 观察状态响应式、抗配置变更、易测试是当前主流MVI在 MVVM 基础上强调单一状态 单向数据流MVVM 的一种更严格的演进趋势MVP 因为要手写大量接口、Presenter 还得管 View 引用已逐渐被 MVVM 取代。MVI 可以看作 MVVM 的强化版把单一不可变状态 单向数据流做到极致。参考来源Android 官方文档 - 应用架构指南UI 层ViewModel 概览数据层