MVP — 什么是Model-View-Presenter模式,在iOS和Android中的应用

作者: IT Sectr 发布日期: 2026-02-16 阅读时间: 10 分钟

MVP(Model-View-Presenter)——一种架构模式,其中Presenter通过ViewContract接口充当Model和View之间的中介。与MVC不同,Controller通过UIKit直接管理View,Presenter不依赖于框架——它通过抽象层工作,这使得它在没有Android SDK或UIKit的情况下可测试。MVP在Jetpack出现之前被广泛用于Android开发,并且仍然适用于遗留项目。更多信息请参阅Martin Fowler的文章

要点

  • MVP——三个组件:Model(数据)、View(界面)、Presenter(逻辑和状态)
  • ViewContract——Presenter通过它与View通信的接口,确保可测试性
  • Presenter——包含所有业务逻辑,不依赖于Android/iOS平台类
  • Passive View——View最大限度地被动,只根据Presenter的指令显示数据
  • MVP vs MVC——Presenter通过单元测试进行测试,MVC中的Controller依赖于UIKit/Android Framework

什么是MVP:Model-View-Presenter模式的本质

MVP(Model-View-Presenter)——由Martin Fowler在2000年代初期提出的架构模式,作为MVC的演进,旨在提高用户界面的可测试性。Model管理数据和业务逻辑,View负责显示和处理用户输入,Presenter——核心组件,接收来自View的事件,从Model提取数据并形成要显示的状态。

MVP与MVC的主要区别——Presenter没有对View的直接引用。相反,Presenter通过ViewContract接口与View交互。View实现此接口并将自身传递给Presenter。这打破了对UIKit(iOS)或Android Framework的依赖——Presenter通过与ViewContract的mock实现进行隔离测试。在MVC中,controller UIViewController直接更新UILabel,在MVP中,Presenter调用view.showName(name)方法,View决定如何显示。

组件职责可测试性
Model数据、业务逻辑、网络调用单元测试(不依赖于UI)
View显示UI、将事件传递给Presenter通过接口进行Mock实现
Presenter业务逻辑、状态管理、导航单元测试(通过ViewContract mock)

单一职责原则在MVP中比在MVC中更为严格:View只负责渲染,Model——负责数据,Presenter——负责逻辑和协调。在实际项目中,Presenter占屏幕代码的40–60%,View——20–30%,Model——20–30%。这种划分允许在不启动Android模拟器或iOS模拟器的情况下测试关键业务逻辑。

Android中的MVP:Presenter、ViewContract和Activity

Android中的MVP使用Activity或Fragment作为View,实现ViewContract——一个带有数据显示方法的接口。Presenter在Activity中创建,将View绑定到自身并管理数据加载。当屏幕旋转时,Activity重新创建——Presenter可以通过retain-fragment或外部存储保留,这解决了纯MVC中典型的状态丢失问题。

kotlin
// ViewContract — Presenter与View通信的接口
interface UserView {
    fun showLoading()
    fun hideLoading()
    fun showUser(user: User)
    fun showError(message: String)
}

// Presenter — 可测试的逻辑层
class UserPresenter(
    private val repository: UserRepository
) {
    private var view: UserView? = null

    fun attachView(view: UserView) {
        this.view = view
    }

    fun detachView() {
        view = null
    }

    fun loadUser(userId: Int) {
        view?.showLoading()
        repository.getUser(userId) { result ->
            view?.hideLoading()
            result.onSuccess { user ->
                view?.showUser(user)
            }.onFailure { e ->
                view?.showError(e.message ?: "未知错误")
            }
        }
    }
}

// View(Activity)实现接口
class UserActivity : AppCompatActivity(), UserView {
    private val presenter = UserPresenter(UserRepository())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        presenter.attachView(this)
        presenter.loadUser(42)
    }

    override fun onDestroy() {
        presenter.detachView()
        super.onDestroy()
    }

    override fun showUser(user: User) { /* 更新 UI */ }
    override fun showLoading() { /* 显示 ProgressBar */ }
    override fun hideLoading() { /* 隐藏 ProgressBar */ }
    override fun showError(message: String) { /* 显示 Snackbar */ }
}

生命周期管理——Android中MVP的关键问题。Activity在屏幕旋转时被销毁,presenter.attachView()在onCreate()中重新调用。如果数据加载是异步的(RxJava、协程),在完成时View可能已分离。解决方案——在detachView()中取消订阅或使用Support Library中的Loader(适用于没有Jetpack的项目)。在IT Sectr,我们在商业项目中已多年使用MVP + RxJava组合——该模式稳定,但需要在订阅管理中严格自律。

Retain-fragment——屏幕旋转时保留Presenter的机制。没有UI的Fragment(setRetainInstance(true))比Activity存活更久并保持对Presenter的引用。当Activity重新创建时,fragment将相同的Presenter传递给新的Activity。Retain-fragment从AndroidX起已弃用,但其pre-Jetpack对应项(Fragment.setRetainInstance)在遗留项目中仍然有效。在现代开发中,Google推荐使用ViewModel代替retain-fragment。

iOS中的MVP:Presenter和View Protocol

iOS中的MVP通过View协议构建。UIViewController实现协议,Presenter不导入UIKit并且纯粹可测试。与Apple MVC不同,其中UIViewController本身包含逻辑和直接的IBOutlet连接,Presenter管理状态并通过协议方法命令View。View不做决定——它执行Presenter的命令:showUser、showLoading、navigateToProfile。

swift
import Foundation

// View Protocol — 供Presenter使用的抽象
protocol UserViewProtocol: AnyObject {
    func showLoading()
    func hideLoading()
    func display(user: User)
    func displayError(message: String)
}

// Presenter — 纯逻辑,无需UIKit
final class UserPresenter {
    private weak var view: UserViewProtocol?
    private let service: UserServiceProtocol

    init(service: UserServiceProtocol) {
        self.service = service
    }

    func attach(view: UserViewProtocol) {
        self.view = view
    }

    func detach() {
        view = nil
    }

    func loadUser(id: Int) {
        view?.showLoading()
        service.fetchUser(id: id) { [weak self] result in
            guard let self else { return }
            self.view?.hideLoading()
            switch result {
            case .success(let user):
                self.view?.display(user: user)
            case .failure(let error):
                self.view?.displayError(message: error.localizedDescription)
            }
        }
    }
}

// View(UIViewController)实现协议
final class UserViewController: UIViewController, UserViewProtocol {
    private let presenter = UserPresenter(service: UserService())

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.attach(view: self)
        presenter.loadUser(id: 42)
    }

    func display(user: User) {
        nameLabel.text = user.name
    }
    // ... 协议的其余方法
}

对View的弱引用(Weak reference)——iOS MVP中的必要条件。UIViewController可能被销毁(从导航栈中弹出),其在Presenter中的闭包将产生循环引用。弱引用(weak var)确保View在离开屏幕时被释放,无论Presenter中是否存在异步操作。在Android中,类似的问题通过detachView()解决——在onDestroy()中的调用将取消对View的引用。

Passive View vs Supervising Controller——Martin Fowler的两种MVP变体。Passive View:View不包含逻辑,Presenter完全管理状态。Supervising Controller:View自己进行简单的数据绑定(例如通过data binding),Presenter只干预复杂场景。在移动开发中,Passive View更常用——它提供最大的可测试性和屏幕状态的可预测性。

MVP与MVC的区别及测试优势

MVP和MVC的主要区别——与View的通信方式。在MVC中,Controller对View有直接引用(UIViewController.IBOutlets、Activity.findViewById)。在MVP中,Presenter通过ViewContract接口与View交互。这种区别从根本上改变了可测试性:实现ViewContract的Mock对象可以在不启动应用程序、模拟器和UI框架的情况下检查Presenter的逻辑。

标准MVCMVP
与View的连接直接(Controller → View)通过接口(Presenter → ViewContract)
逻辑测试需要UIKit/Android Framework无平台依赖的单元测试
生命周期Controller与屏幕共存Presenter可以存活更久(retain)
复杂性最小每个屏幕+1个接口
Massive Controller典型问题逻辑在Presenter中,View薄

Presenter单元测试示例(Kotlin):创建mock UserView,传递给Presenter,调用loadUser,检查showUser是否用正确的数据被调用。测试在毫秒内执行,不需要模拟器。在iOS上类似——OCMock或协议stub检查UserViewProtocol方法的调用。在IT Sectr使用MVP的项目中,业务逻辑的单元测试覆盖率达到了85–90%,比类似的MVC项目高2–3倍。

何时MVP优先于MVC——在对稳定性有严格要求的项目中:银行应用、医疗系统、支付终端。在这些领域,错误成本很高,单元测试至关重要。在Post-MVP项目中(当产品已在市场上但代码库是遗留代码时),MVP允许逐步将逻辑从Massive View Controller移到可测试层,而无需完全重写架构。

MVP的局限性及向MVVM的过渡

MVP的主要缺点——接口数量增加和手动订阅管理。每个屏幕至少需要一个ViewContract + Presenter,50个屏幕——50个接口和50个Presenter类。在MVVM中,ViewModel取代Presenter并使用响应式机制(LiveData、StateFlow、ObservableObject),这消除了手动attach/detach和ViewContract接口的需求。

RxJava和MVP——Android 2015–2019流行的组合。Presenter订阅Repository中的Observable,通过ViewContract显示结果。问题:disposable必须在detachView()中显式取消,否则订阅泄漏将在更新分离的View时导致崩溃。RxLifecycle和AutoDispose库部分自动化了取消,但增加了依赖。在IT Sectr,我们于2020年从MVP+RxJava迁移到MVVM+Flow——由于ViewContract的消失,代码缩短了25–30%。

从MVP迁移到MVVM——分阶段过程。1)在Presenter中用LiveData/StateFlow替换ViewContract。2)删除attach/detach方法——通过observe()进行订阅。3)将Presenter重命名为ViewModel。4)为ViewModelFactory集成DI(Hilt/Koin)。一个屏幕的迁移需要2–4小时,整个代码库——对于50–100个屏幕的项目需要2–4周。迁移后,ViewContract接口被删除,代码缩短,测试保留。

现代开发中的MVP——该模式仍然存在,但落后于MVVM和MVI。Google官方推荐新项目使用MVVM with Jetpack。Apple——MVVM with SwiftUI。然而,了解MVP对于与遗留代码工作是必要的:Google Play中有数百个Android应用仍在MVP上运行,包括大型银行、零售商和运输公司的应用。理解MVP是掌握MVI和Clean Architecture的基础,因为Presenter是Robert Martin术语中Use Case的直接前身。

常见问题

MVP与MVC有何不同?

在MVP中,Presenter通过ViewContract接口与View交互,而不是直接交互。在MVC中,Controller通过IBOutlet/findViewById直接引用View。MVP允许在没有iOS Simulator或Android Emulator的情况下通过单元测试测试Presenter,因为Presenter不依赖于UIKit或Android Framework。MVC需要启动应用程序来测试controller。

何时应该使用MVP而不是MVVM?

MVP适用于已基于此模式构建的遗留项目以及不支持响应式机制(LiveData、StateFlow、Combine)的应用。对于新项目,Google推荐MVVM with Jetpack(Android),Apple推荐MVVM with SwiftUI(iOS)。在需要单元测试业务逻辑且没有Combine的纯UIKit项目中,MVP仍然是最佳选择。

如何解决屏幕旋转时Presenter丢失的问题?

在Android上——使用retain-fragment(setRetainInstance(true))或Jetpack中的ViewModel。Retain-fragment在旋转时保留Presenter并将其传递给新的Activity。Google的ViewModel——现代替代方案,在旋转时自动保留状态,无需retain-fragment。在iOS上——Presenter在每个viewDidLoad时重新创建,但在单独的协调器服务中进行缓存。

MVP中一个屏幕需要多少个类?

最少4个:ViewContract接口、ViewContract实现(Activity/Fragment)、Presenter、Model(Repository)。如果使用Dagger/Hilt,还会添加DI模块。对于50个屏幕,那是200+个类。MVVM每屏幕减少1个文件(不需要ViewContract),MVI添加State和Intent类。类的数量——在大型项目中反对MVP的主要论据。

在MVP中,Passive View和Supervising Controller有什么区别?

Passive View——View不包含逻辑,Presenter完全管理状态和数据。Supervising Controller——View自己进行简单的绑定(data binding),Presenter在复杂场景中干预。在移动开发中,Passive View占主导地位——提供最大的可测试性和可预测性。Supervising Controller用于Web框架(ASP.NET Web Forms、GWT)。

总结

  • MVP(Model-View-Presenter)——MVC的演进,通过ViewContract接口具有可测试的Presenter层
  • ViewContract——将View从Presenter抽象化接口,允许mock测试
  • Passive View——移动开发中占主导地位的MVP变体,View被动
  • Presenter——包含业务逻辑,不依赖于UIKit或Android Framework
  • MVP vs MVC——MVP解决了测试问题,但每个屏幕增加了1个接口
  • Android retain-fragment——在Jetpack ViewModel出现之前屏幕旋转时保留Presenter
  • 向MVVM迁移——用LiveData/StateFlow替换ViewContract,代码缩短25–30%

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读