MVC、MVP、MVVM 架构模式

这三种模式都是为了解决同一个核心问题:将 UI(视图)与业务逻辑(模型)解耦,让代码更易维护、可测试。


MVC(Model - View - Controller)

角色分工

角色在 Android 中职责
Model数据层(网络、数据库、Repository)数据的获取与处理
ViewXML 布局文件负责界面展示
ControllerActivity / 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 中职责
ModelRepository / DataSource数据的获取与处理
ViewActivity / 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 的痛点

  1. 接口爆炸:每个页面都要写一套 Contract 接口,随着功能增加,接口方法越来越多,维护成本高。
  2. 内存泄漏风险:Presenter 持有 View 接口(即 Activity)的引用,必须手动在 onDestroy 中调用 detachView(),稍不注意就会泄漏。
  3. 生命周期不感知:Presenter 对 Activity 的生命周期一无所知,屏幕旋转后 Presenter 会被销毁并重建,无法保留状态。

MVVM(Model - View - ViewModel)

角色分工

角色在 Android 中职责
ModelRepository / DataSource数据的获取与处理
ViewActivity / Fragment观察数据变化,更新 UI
ViewModelandroidx.lifecycle.ViewModel持有 UI 状态,处理业务逻辑;生命周期长于 View

核心思想

MVVM 的关键是数据驱动 UI(Data Binding / Observer 模式)。ViewModel 不持有 View 的任何引用,View 单向观察 ViewModel 中的数据(LiveDataStateFlow),数据变化时 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 官方推荐的最佳实践。


三者横向对比

维度MVCMVPMVVM
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 中,而 ViewModelStoreNonConfigurationInstances(一个系统级别的"配置变更幸存区")持有。

Activity 因为配置变更(旋转)而重建时,系统会把旧 Activity 的 NonConfigurationInstances 传给新 Activity。新 Activity 通过 by viewModels {} 获取 ViewModel 时,ViewModelProvider 先在 ViewModelStore 中查询是否已有实例,有则直接返回,于是拿到了和旧 Activity 相同的 ViewModel 实例

只有当用户真正退出 Activity(按 Back 键,或 finish() 被调用时),ViewModelStore 才会调用 ViewModel.onCleared(),ViewModel 才会被销毁。

3. LiveData 和 StateFlow 应该选哪个?

对比维度LiveDataStateFlow
生命周期感知内置(需传入 LifecycleOwner)需配合 repeatOnLifecycle
初始值可无初始值必须有初始值
粘性事件有(新观察者会收到最后一个值)
线程安全postValue 线程安全MutableStateFlow.value 线程安全
可组合性弱(操作符少)强(Flow 全套操作符)
Compose 支持通过 observeAsState() 转换原生支持(collectAsStateWithLifecycle

结论: 新项目推荐 StateFlow(配合 repeatOnLifecycle),尤其是使用 Jetpack Compose 的项目。老项目 LiveData 足够用,不需要强行迁移。一次性事件(如 Toast、导航跳转)用 SharedFlowChannel,而不是 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
    }
}