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)——由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使用Activity或Fragment作为View,实现ViewContract——一个带有数据显示方法的接口。Presenter在Activity中创建,将View绑定到自身并管理数据加载。当屏幕旋转时,Activity重新创建——Presenter可以通过retain-fragment或外部存储保留,这解决了纯MVC中典型的状态丢失问题。
// 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通过View协议构建。UIViewController实现协议,Presenter不导入UIKit并且纯粹可测试。与Apple MVC不同,其中UIViewController本身包含逻辑和直接的IBOutlet连接,Presenter管理状态并通过协议方法命令View。View不做决定——它执行Presenter的命令:showUser、showLoading、navigateToProfile。
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的主要区别——与View的通信方式。在MVC中,Controller对View有直接引用(UIViewController.IBOutlets、Activity.findViewById)。在MVP中,Presenter通过ViewContract接口与View交互。这种区别从根本上改变了可测试性:实现ViewContract的Mock对象可以在不启动应用程序、模拟器和UI框架的情况下检查Presenter的逻辑。
| 标准 | MVC | MVP |
|---|---|---|
| 与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的主要缺点——接口数量增加和手动订阅管理。每个屏幕至少需要一个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中,Presenter通过ViewContract接口与View交互,而不是直接交互。在MVC中,Controller通过IBOutlet/findViewById直接引用View。MVP允许在没有iOS Simulator或Android Emulator的情况下通过单元测试测试Presenter,因为Presenter不依赖于UIKit或Android Framework。MVC需要启动应用程序来测试controller。
MVP适用于已基于此模式构建的遗留项目以及不支持响应式机制(LiveData、StateFlow、Combine)的应用。对于新项目,Google推荐MVVM with Jetpack(Android),Apple推荐MVVM with SwiftUI(iOS)。在需要单元测试业务逻辑且没有Combine的纯UIKit项目中,MVP仍然是最佳选择。
在Android上——使用retain-fragment(setRetainInstance(true))或Jetpack中的ViewModel。Retain-fragment在旋转时保留Presenter并将其传递给新的Activity。Google的ViewModel——现代替代方案,在旋转时自动保留状态,无需retain-fragment。在iOS上——Presenter在每个viewDidLoad时重新创建,但在单独的协调器服务中进行缓存。
最少4个:ViewContract接口、ViewContract实现(Activity/Fragment)、Presenter、Model(Repository)。如果使用Dagger/Hilt,还会添加DI模块。对于50个屏幕,那是200+个类。MVVM每屏幕减少1个文件(不需要ViewContract),MVI添加State和Intent类。类的数量——在大型项目中反对MVP的主要论据。
Passive View——View不包含逻辑,Presenter完全管理状态和数据。Supervising Controller——View自己进行简单的绑定(data binding),Presenter在复杂场景中干预。在移动开发中,Passive View占主导地位——提供最大的可测试性和可预测性。Supervising Controller用于Web框架(ASP.NET Web Forms、GWT)。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。