Android 架构演进:手把手带你理解并落地 MVP 模式

随着 Android 应用规模的不断扩大,传统的 MVC 模式(Model-View-Controller)在 Android 开发中逐渐显得力不从心。由于 Android 系统的特殊设计,Activity/Fragment 常常需要同时兼顾布局生命周期与业务数据请求,导致代码越写越臃肿,变成了难以维护的“万能类”(Fat Activity)。

为了解决关注点分离(Separation of Concerns)便捷的单元测试Model-View-Presenter (MVP) 架构模式应运而生。本文将基于谷歌官方经典的 todo-mvp 示例,带你一步步落地并吃透 MVP 模式。


一、 什么是 MVP?

MVP 的全称为 Model-View-Presenter,它将应用拆分为三个核心部分:Model 提供数据与核心业务,View 负责 UI 渲染与用户交互,Presenter 负责两者之间的调度逻辑。

+------------------+         +-------------------+         +-----------------+
|                  |  Calls  |                   |  Calls  |                 |
|      View        |-------->|     Presenter     |-------->|     Model       |
| (Activity/Frag)  |<--------|                   |<--------| (Repo/Data)     |
|                  | Updates |                   | Events  |                 |
+------------------+         +-------------------+         +-----------------+

根据维基百科的模型描述:

  • Model(模型):定义用户界面所需要显示的资料模型,包含相关的业务数据逻辑(如网络请求、数据库操作、本地缓存等)。
  • View(视图):呈现用户界面的终端,用以表现来自 Model 的数据,并将用户触发的命令事件路由传给 Presenter 对齐处理。在 Android 中,View 通常由 Activity、Fragment 或自定义 View 来充当。
  • Presenter(呈递者):View 与 Model 之间的纽带。它负责检索 Model 获取数据,并将获取的数据经过格式转换(数据清洗)后交由 View 进行呈现;同时,它也消费 View 传递过来的用户事件。

MVP 与 MVC 的本质区别:被动视图 (Passive View)

在经典的 MVC 中,Model 的改变可以直接通知 View 更新。而在 MVP 中,View 与 Model 彻底解耦,它们之间不存在任何直接联系,所有的交互都必须通过 Presenter 作为中介来进行。此时的 View 变成了纯粹的“被动视图”——只负责听从 Presenter 的指令改变 UI,或者将手指触摸事件直接汇报给 Presenter。

MVP 流程图示


二、 谷歌官方 MVP 设计规范与实现

以下代码参考谷歌官方架构样板 To-DoApp的todo-mvp-kotlin分支

1. 定义基础基类 BaseViewBasePresenter

为了规范化契约绑定,我们定义了两个基础接口:

interface BaseView<T> {
    // 每个 View 都持有一个 presenter 的引用,便于交互
    var presenter: T
}
interface BasePresenter {
    // 规定 Presenter 启动的时机,通常在 View 的 onResume 阶段调用以加载初始数据
    fun start()
}

2. 编写功能契约:Contract 机制

谷歌官方 MVP 推荐使用一个 Contract(契约类) 接口将特定页面下的 ViewPresenter 接口放在一起。这样做的好处是一目了然,任何人点开契约类都能立刻明白这个页面有哪些 UI 展现形式,支持哪些用户操作。

Task 详情页 为例,用户能执行 4 种操作,分别是编辑 Task、删除 Task、改变 Task 完成状态(已完成/未完成)。我们定义 TaskDetailContract 如下:

interface TaskDetailContract {
 
    interface Presenter : BasePresenter {
        // 用户操作:编辑 Task
        fun editTask()
        // 用户操作:删除 Task
        fun deleteTask()
        // 用户操作:完成 Task
        fun completeTask()
        // 用户操作:激活 Task
        fun activateTask()
    }
 
    interface View : BaseView<Presenter> {
        val isActive: Boolean
        
        // UI 更新:是否显示加载中进度条
        fun setLoadingIndicator(active: Boolean)
        // UI 更新:显示找不到 Task 错误
        fun showMissingTask()
        // UI 更新:Task为空时,隐藏标题
        fun hideTitle()
        // UI 更新:显示标题
        fun showTitle(title: String)
        // UI 更新:Task为空时,隐藏描述
        fun hideDescription()
        // UI 更新:显示描述
        fun showDescription(description: String)
        // UI 更新:显示Task的完成状态
        fun showCompletionStatus(complete: Boolean)
        // UI 更新:跳转到编辑Task的页面
        fun showEditTask(taskId: String)
        // UI 更新:执行Task删除后的UI跳转逻辑
        fun showTaskDeleted()
        // UI 更新:显示Task已完成的提示界面
        fun showTaskMarkedComplete()
        // UI 更新:显示Task已激活的提示界面
        fun showTaskMarkedActive()
    }
}

三、 业务代码交互分析

在实际的业务中,Activity 充当装载容器,具体实现逻辑写在 Fragment 中,Presenter 负责桥接。

1. Activity 层:组件装配与绑定

TaskDetailActivity 作为入口容器,在 onCreate 时实例化 TaskDetailFragment(实现了 View 接口)和 TaskDetailPresenter,并将它们绑定在一起。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.taskdetail_act)
 
    // 1. 获取或创建 TaskDetailFragment 实例
    val taskDetailFragment = supportFragmentManager
        .findFragmentById(R.id.contentFrame) as TaskDetailFragment?
        ?: TaskDetailFragment.newInstance(taskId).also {
            replaceFragmentInActivity(it, R.id.contentFrame)
        }
 
    // 2. 实例化 Presenter,并将 View 作为参数传过去完成依赖注入
    TaskDetailPresenter(
        taskId,
        Injection.provideTasksRepository(applicationContext),
        taskDetailFragment
    )
}

2. Presenter 层:接收 View 并反向绑定

TaskDetailPresenter 接收了实现了 View 接口的 Fragment。在构造方法中,通过 taskDetailView.presenter = this 将自身引用回传给 View,完成了双向绑定

class TaskDetailPresenter(
    private val taskId: String,
    private val tasksRepository: TasksRepository,
    private val taskDetailView: TaskDetailContract.View
) : TaskDetailContract.Presenter {
 
    init {
        // 完成双向引用的绑定
        taskDetailView.presenter = this
    }
 
    override fun start() {
        openTask()
    }
 
    private fun openTask() {
        if (taskId.isEmpty()) {
            taskDetailView.showMissingTask()
            return
        }
        taskDetailView.setLoadingIndicator(true)
        tasksRepository.getTask(taskId, object : TasksDataSource.GetTaskCallback {
            override fun onTaskLoaded(task: Task) {
                if (!taskDetailView.isActive) return
                taskDetailView.setLoadingIndicator(false)
                showTask(task)
            }
 
            override fun onDataNotAvailable() {
                if (!taskDetailView.isActive) return
                taskDetailView.setLoadingIndicator(false)
                taskDetailView.showMissingTask()
            }
        })
    }
    // ...
}

3. Fragment(View)层:生命周期对齐与命令传递

TaskDetailFragment 实现了 View 接口,并持有 presenter 引用。在生命周期 onResume() 中,调用 presenter.start() 触发 Presenter 去 Model 请求数据。

class TaskDetailFragment : Fragment(), TaskDetailContract.View {
    
    override lateinit var presenter: TaskDetailContract.Presenter
 
    override fun onResume() {
        super.onResume()
        // 对齐生命周期,启动数据加载
        presenter.start()
    }
    
    // ...
}

四、 核心交互流程实战:以“删除 Task”为例

为了让你彻底看清 MVP 的数据流向,我们以用户点击删除菜单为例:

步骤 1:View 接收用户点击,路由事件给 Presenter

TaskDetailFragment 的菜单点击事件中,不去执行任何具体的删除代码,而是直接汇报给 Presenter:

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    val deletePressed = item.itemId == R.id.menu_delete
    if (deletePressed) {
        // 告诉主事者:用户想删掉这个任务
        presenter.deleteTask()
    }
    return deletePressed
}

步骤 2:Presenter 负责业务调度,并指示 View 改变 UI

TaskDetailPresenter 接收到命令,开始进行条件判定并指挥 Model 执行物理删除,然后回调 View 刷新界面:

override fun deleteTask() {
    if (taskId.isEmpty()) {
        taskDetailView.showMissingTask()
        return
    }
    // 1. 调用 Model 层(Repository)执行物理删除
    tasksRepository.deleteTask(taskId)
    // 2. 指挥 View 层更新 UI 状态
    taskDetailView.showTaskDeleted()
}

步骤 3:View 执行最纯粹的 UI 逻辑

TaskDetailFragment 听从指挥,执行纯粹的 UI 关机或提示操作:

override fun showMissingTask() {
    detailTitle.text = ""
    detailDescription.text = getString(R.string.no_data)
}
 
override fun showTaskDeleted() {
    // 纯粹的 UI 关闭逻辑
    activity?.finish()
}

五、 MVP 的优缺点与设计反思

1. 优势

  • 极佳的可测试性:因为 Presenter 层完全不依赖任何 Android 视图框架类(即没有 import android.view.*R),我们可以直接对 Presenter 编写纯 JUnit 单元测试,不需要依赖 Robolectric 或真机设备。
  • 高内聚低耦合:业务逻辑收拢到 Presenter,UI 的视觉操作收拢到 Fragment。即便以后将布局从传统 XML 迁移到 Jetpack Compose,我们也只需要替换 View 层实现,Presenter 层的业务逻辑不用做任何改动。

2. 缺点

  • 接口文件爆炸:每个页面都需要写一个 Contract 契约类和多个内部接口,前期开发会伴随着大量的样板接口代码。
  • Presenter 容易膨胀:如果页面非常复杂,Presenter 同样会面对代码量过大的问题。
  • 内存泄漏隐患:由于 Presenter 持有了 View(如 Fragment)的引用,而网络数据请求等后台线程可能在页面销毁时仍未返回,极易导致 Activity 无法被回收。因此在实际的工业开发中,我们需要在 Presenter 中增加类似 detachView() 的注销机制,或者利用 Jetpack Lifecycle 自动绑定销毁行为。