MVC、MVP、MVVM 架构模式
这三种模式都是为了解决同一个核心问题:将 UI(视图)与业务逻辑(模型)解耦,让代码更易维护、可测试。
MVC(Model - View - Controller)
角色分工
| 角色 | 在 Android 中 | 职责 |
|---|---|---|
| Model | 数据层(网络、数据库、Repository) | 数据的获取与处理 |
| View | XML 布局文件 | 负责界面展示 |
| Controller | Activity / Fragment | 接收用户输入,协调 Model 和 View |
核心问题
Android 中 MVC 的致命缺陷是:Activity/Fragment 既是 Controller 又是 View。
Activity 本身要 setContentView(View 的职责),又要处理点击事件与数据请求(Controller 的职责),导致 Activity 越来越臃肿。
// 典型的 MVC Activity,臃肿的 "上帝类"
class UserActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user) // View 的职责
btnLoad.setOnClickListener { // Controller 的职责:响应事件
// 直接调用网络,夹杂在 Activity 中
UserApi.getUser(userId) { user -> // Model 的职责
tvName.text = user.name // 又直接操作 View
}
}
}
}
问题总结:
- Activity 臃肿,难以维护(几千行是常态)
- View 和 Controller 耦合严重,难以单元测试
- Model 回调直接操作 View,导致层与层之间边界模糊
MVP(Model - View - Presenter)
角色分工
| 角色 | 在 Android 中 | 职责 |
|---|---|---|
| Model | Repository / DataSource | 数据的获取与处理 |
| View | Activity / Fragment(实现 View 接口) | 只负责 UI 展示,不含业务逻辑 |
| Presenter | 普通 Kotlin/Java 类 | 持有 View 接口和 Model,处理所有业务逻辑 |
核心思想
用一个 接口(Contract) 将 View 与 Presenter 解耦。Presenter 通过接口操作 View,完全不依赖 Android Framework,可以独立进行单元测试。
// 1. Contract 接口:定义 View 和 Presenter 的契约
interface UserContract {
interface View {
fun showLoading()
fun hideLoading()
fun showUser(name: String)
fun showError(msg: String)
}
interface Presenter {
fun loadUser(userId: String)
fun detachView()
}
}
// 2. Presenter:纯 Kotlin 类,不依赖 Android,可单元测试
class UserPresenter(
private var view: UserContract.View?,
private val repository: UserRepository
) : UserContract.Presenter {
override fun loadUser(userId: String) {
view?.showLoading()
repository.getUser(userId,
onSuccess = { user ->
view?.hideLoading()
view?.showUser(user.name)
},
onError = { e ->
view?.hideLoading()
view?.showError(e.message ?: "未知错误")
}
)
}
// ⚠️ 必须在 Activity onDestroy 时调用,防止内存泄漏
override fun detachView() {
view = null
}
}
// 3. Activity:只负责 UI,实现 View 接口
class UserActivity : AppCompatActivity(), UserContract.View {
private lateinit var presenter: UserPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
presenter = UserPresenter(this, UserRepository())
btnLoad.setOnClickListener { presenter.loadUser("123") }
}
override fun showLoading() { progressBar.visibility = View.VISIBLE }
override fun hideLoading() { progressBar.visibility = View.GONE }
override fun showUser(name: String) { tvName.text = name }
override fun showError(msg: String) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() }
override fun onDestroy() {
super.onDestroy()
presenter.detachView() // 解除引用,防止泄漏
}
}
MVP 的痛点
- 接口爆炸:每个页面都要写一套 Contract 接口,随着功能增加,接口方法越来越多,维护成本高。
- 内存泄漏风险:Presenter 持有 View 接口(即 Activity)的引用,必须手动在
onDestroy中调用detachView(),稍不注意就会泄漏。 - 生命周期不感知:Presenter 对 Activity 的生命周期一无所知,屏幕旋转后 Presenter 会被销毁并重建,无法保留状态。
MVVM(Model - View - ViewModel)
角色分工
| 角色 | 在 Android 中 | 职责 |
|---|---|---|
| Model | Repository / DataSource | 数据的获取与处理 |
| View | Activity / Fragment | 观察数据变化,更新 UI |
| ViewModel | androidx.lifecycle.ViewModel | 持有 UI 状态,处理业务逻辑;生命周期长于 View |
核心思想
MVVM 的关键是数据驱动 UI(Data Binding / Observer 模式)。ViewModel 不持有 View 的任何引用,View 单向观察 ViewModel 中的数据(LiveData 或 StateFlow),数据变化时 UI 自动更新。
// 1. Repository:数据层,不依赖 Android
class UserRepository {
suspend fun getUser(userId: String): Result<User> {
return runCatching { UserApi.service.getUser(userId) }
}
}
// 2. ViewModel:持有 UI 状态,不持有 View 引用
class UserViewModel(private val repository: UserRepository) : ViewModel() {
// UI 状态用密封类统一管理
sealed class UiState {
object Loading : UiState()
data class Success(val name: String) : UiState()
data class Error(val message: String) : UiState()
}
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch { // 生命周期与 ViewModel 绑定,ViewModel 销毁时自动取消
_uiState.value = UiState.Loading
repository.getUser(userId)
.onSuccess { user -> _uiState.value = UiState.Success(user.name) }
.onFailure { e -> _uiState.value = UiState.Error(e.message ?: "未知错误") }
}
}
}
// 3. Activity/Fragment:只负责观察数据和更新 UI
class UserActivity : AppCompatActivity() {
// ViewModel 由系统管理,屏幕旋转后自动恢复,不会重建
private val viewModel: UserViewModel by viewModels {
ViewModelProvider.Factory { UserViewModel(UserRepository()) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 观察 UI 状态,lifecycleScope 保证生命周期安全
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is UserViewModel.UiState.Loading -> showLoading()
is UserViewModel.UiState.Success -> showUser(state.name)
is UserViewModel.UiState.Error -> showError(state.message)
}
}
}
}
btnLoad.setOnClickListener { viewModel.loadUser("123") }
}
}
ViewModel 的特殊生命周期
ViewModel 与 ViewModelStore 绑定,生命周期长于 Activity。屏幕旋转时 Activity 会销毁重建,但 ViewModel 不会重建,数据在旋转后仍然存在。
屏幕旋转:
Activity.onDestroy() → ViewModel 仍然存活
Activity.onCreate() → by viewModels {} 返回同一个 ViewModel 实例
ViewModel.onCleared() → 只有用户真正退出(Back 键)才调用
为什么用 repeatOnLifecycle?
直接用 lifecycleScope.launch { flow.collect { } } 有一个问题:当 Activity 进入后台(onStop),Flow 仍在收集,消耗资源,且后台更新 UI 可能崩溃。
repeatOnLifecycle(Lifecycle.State.STARTED) 会:
- Activity 进入
STARTED时开启收集 - Activity 进入
STOPPED时自动取消收集 - Activity 再次回到
STARTED时重新开启收集
这是 Google 官方推荐的最佳实践。
三者横向对比
| 维度 | MVC | MVP | MVVM |
|---|---|---|---|
| View 对 Model 的依赖 | 直接依赖 | 通过 Presenter 隔离 | 完全解耦(观察者模式) |
| 单元测试 | 困难(Activity 耦合) | 容易(Presenter 可独立测试) | 容易(ViewModel 可独立测试) |
| 内存泄漏风险 | 高 | 中(需手动 detachView) | 低(不持有 View 引用) |
| 生命周期感知 | 无 | 无 | 内置(ViewModel + Lifecycle) |
| 屏幕旋转状态保留 | 需手动 onSaveInstanceState | 需手动处理 | 自动(ViewModel 存活) |
| 代码量 / 模板代码 | 少 | 多(大量接口) | 中(无需接口,但需理解响应式) |
| 适用场景 | 简单页面、快速原型 | 中等复杂度项目 | 推荐用于现代 Android 开发 |
常见问题
1. MVP 和 MVVM 最本质的区别是什么?
MVP:命令式(Imperative)。Presenter 拿到数据后,主动调用 View 的接口方法更新 UI(view.showUser(name)),是一种"推送命令"的关系,View 是被动接收者。
MVVM:响应式(Reactive)。ViewModel 更新数据状态(_uiState.value = ...),View 观察状态变化并被动响应,是一种"订阅数据"的关系,ViewModel 完全不知道 View 的存在。
本质区别在于:MVP 中 Presenter 知道 View 是谁(持有接口引用),MVVM 中 ViewModel 完全不知道 View 是谁。
2. ViewModel 为什么能在屏幕旋转后存活?
ViewModel 的实例存储在 ViewModelStore 中,而 ViewModelStore 被 NonConfigurationInstances(一个系统级别的"配置变更幸存区")持有。
Activity 因为配置变更(旋转)而重建时,系统会把旧 Activity 的 NonConfigurationInstances 传给新 Activity。新 Activity 通过 by viewModels {} 获取 ViewModel 时,ViewModelProvider 先在 ViewModelStore 中查询是否已有实例,有则直接返回,于是拿到了和旧 Activity 相同的 ViewModel 实例。
只有当用户真正退出 Activity(按 Back 键,或 finish() 被调用时),ViewModelStore 才会调用 ViewModel.onCleared(),ViewModel 才会被销毁。
3. LiveData 和 StateFlow 应该选哪个?
| 对比维度 | LiveData | StateFlow |
|---|---|---|
| 生命周期感知 | 内置(需传入 LifecycleOwner) | 需配合 repeatOnLifecycle |
| 初始值 | 可无初始值 | 必须有初始值 |
| 粘性事件 | 有(新观察者会收到最后一个值) | 有 |
| 线程安全 | postValue 线程安全 | MutableStateFlow.value 线程安全 |
| 可组合性 | 弱(操作符少) | 强(Flow 全套操作符) |
| Compose 支持 | 通过 observeAsState() 转换 | 原生支持(collectAsStateWithLifecycle) |
结论: 新项目推荐 StateFlow(配合 repeatOnLifecycle),尤其是使用 Jetpack Compose 的项目。老项目 LiveData 足够用,不需要强行迁移。一次性事件(如 Toast、导航跳转)用 SharedFlow 或 Channel,而不是 StateFlow(会有粘性问题)。
5. 在 MVVM 中,Repository 的职责是什么?
Repository 是 ViewModel 和数据源之间的抽象层,它向上对 ViewModel 暴露干净的领域接口,向下协调多个数据源(网络 API、本地数据库、缓存)。
好处:
- ViewModel 不需要知道数据来自网络还是本地缓存,只调用
repository.getUser(id)即可。 - Repository 可以实现缓存策略(先查本地 Room,本地无数据再请求网络,再写回 Room)。
- 便于单元测试:可以 Mock 整个 Repository,ViewModel 的测试不依赖真实网络。
class UserRepository(
private val api: UserApi, // 远程数据源
private val dao: UserDao // 本地数据库(Room)
) {
suspend fun getUser(userId: String): User {
// 先查本地缓存
val cached = dao.getUserById(userId)
if (cached != null) return cached
// 本地无数据,请求网络并写入数据库
val user = api.getUser(userId)
dao.insert(user)
return user
}
}