MVP (Model-View-Presenter) is an architectural pattern where the Presenter acts as an intermediary between the Model and the View through the ViewContract interface. Unlike MVC, where the Controller directly manages the View through UIKit, the Presenter does not depend on the framework — it works through abstraction, making it testable without Android SDK or UIKit. MVP is widely used in Android development before Jetpack and remains relevant for legacy projects. Read more in Martin Fowler's article.
Key Takeaways
MVP (Model-View-Presenter) is an architectural pattern proposed by Martin Fowler in the early 2000s as an evolution of MVC to improve testability of the user interface. Model manages data and business logic, View handles rendering and user input processing, Presenter is the central component that receives events from View, retrieves data from Model and forms the state for display.
The main difference between MVP and MVC — the Presenter does not have a direct reference to the View. Instead, the Presenter interacts with the View through the ViewContract interface. The View implements this interface and passes itself to the Presenter. This breaks the dependency on UIKit (iOS) or Android Framework — the Presenter can be tested in isolation with a mock implementation of ViewContract. In MVC the UIViewController controller directly updates UILabel, in MVP the Presenter calls view.showName(name), and the View decides how to display it.
| Component | Responsibility | Testability |
|---|---|---|
| Model | Data, business logic, network calls | Unit tests (independent of UI) |
| View | UI rendering, passing events to Presenter | Mock implementation via interface |
| Presenter | Business logic, state management, navigation | Unit tests (via ViewContract mock) |
Single Responsibility Principle in MVP is followed more strictly than in MVC: View is only responsible for rendering, Model for data, Presenter for logic and coordination. In real projects the Presenter takes 40–60% of the screen code, View — 20–30%, Model — 20–30%. This distribution allows testing key business logic without launching an Android emulator or iOS simulator.
MVP in Android uses Activity or Fragment as the View, which implements ViewContract — an interface with data display methods. The Presenter is created in the Activity, attaches the View to itself and manages data loading. When the screen rotates, the Activity is recreated — the Presenter can be preserved through a retain fragment or external storage, solving the state loss problem characteristic of pure MVC.
// ViewContract — interface for Presenter to communicate with View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — testable logic layer
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 ?: "Unknown error")
}
}
}
}
// View (Activity) implements the interface
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) { /* update UI */ }
override fun showLoading() { /* show ProgressBar */ }
override fun hideLoading() { /* hide ProgressBar */ }
override fun showError(message: String) { /* show Snackbar */ }
}
Lifecycle management is a key problem of MVP on Android. The Activity is destroyed on screen rotation, and presenter.attachView() is called again in onCreate(). If data loading is asynchronous (RxJava, coroutines), by the time it completes the View may be detached. The solution is to cancel subscriptions in detachView() or use the Loader from Support Library (for projects without Jetpack). At IT Sectr we used the MVP + RxJava combination for years in commercial projects — the pattern is stable but requires discipline in subscription management.
Retain fragments — a mechanism for preserving the Presenter on screen rotation. A fragment without UI (setRetainInstance(true)) outlives the Activity and holds a reference to the Presenter. When the Activity is recreated, the fragment passes the same Presenter to the new Activity. Retain fragments are deprecated since AndroidX, but their pre-Jetpack analog (Fragment.setRetainInstance) still works in legacy projects. In modern development Google recommends ViewModel instead of retain fragments.
MVP in iOS is built through a View protocol. UIViewController implements the protocol, the Presenter does not import UIKit and is purely testable. Unlike Apple MVC, where UIViewController itself contains logic and direct IBOutlet connections, the Presenter manages state and commands the View through protocol methods. The View does not make decisions — it executes Presenter commands: showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — abstraction for Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — pure logic, no 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) implements the protocol
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
}
// ... rest of protocol methods
}
Weak reference to the View is mandatory in iOS MVP. A UIViewController can be destroyed (pop from navigation stack), and its closure in the Presenter would create a retain cycle. A weak reference (weak var) ensures the View is released when it leaves the screen, regardless of asynchronous operations in the Presenter. In Android a similar problem is solved through detachView() — calling it in onDestroy() nullifies the reference to the View.
Passive View vs Supervising Controller — two MVP variants by Martin Fowler. Passive View: the View contains no logic, the Presenter fully manages the state. Supervising Controller: the View itself does simple data binding (for example, through data binding), the Presenter only intervenes in complex scenarios. In mobile development Passive View is used more often — it provides maximum testability and predictability of the screen state.
The main difference between MVP and MVC is the way of communicating with the View. In MVC the Controller has a direct reference to the View (UIViewController.IBOutlets, Activity.findViewById). In MVP the Presenter interacts with the View through the ViewContract interface. This difference fundamentally changes testability: a mock object implementing ViewContract allows testing the Presenter's logic without launching the app, emulator or UI framework.
| Criterion | MVC | MVP |
|---|---|---|
| View connection | Direct (Controller → View) | Through interface (Presenter → ViewContract) |
| Logic testing | Requires UIKit/Android Framework | Unit tests without platform dependencies |
| Lifecycle | Controller lives with the screen | Presenter can outlive (retain) |
| Complexity | Minimal | +1 interface per screen |
| Massive Controller | Typical problem | Logic in Presenter, View is thin |
Example of a Presenter unit test in Kotlin: a mock UserView is created, passed to the Presenter, loadUser is called, it checks that showUser was called with correct data. The test executes in milliseconds, no emulator required. On iOS similarly — OCMock or a protocol stub verifies UserViewProtocol method calls. In IT Sectr projects with MVP, business logic unit test coverage reached 85–90%, which is 2–3 times higher than in similar MVC projects.
When MVP is preferable to MVC — in projects with strict stability requirements: banking apps, medical systems, payment terminals. In these domains the cost of an error is high, and unit tests are critical. In post-MVP projects (when the product is already on the market but the codebase is legacy), MVP allows gradually extracting logic from Massive View Controller into a testable layer without a complete architectural rewrite.
The main drawbacks of MVP are the growth in the number of interfaces and manual subscription management. Each screen requires at least one ViewContract + Presenter, for 50 screens — 50 interfaces and 50 Presenter classes. In MVVM, ViewModel replaces Presenter and uses reactive mechanisms (LiveData, StateFlow, ObservableObject), eliminating the need for manual attach/detach and ViewContract interfaces.
RxJava and MVP — a popular combination in Android 2015–2019. The Presenter subscribes to an Observable from the Repository, displays the result through ViewContract. The problem: disposable must be explicitly cancelled in detachView(), otherwise a subscription leak will cause a crash when updating a detached View. The RxLifecycle and AutoDispose libraries partially automated unsubscription but added dependencies. At IT Sectr we switched from MVP+RxJava to MVVM+Flow in 2020 — the code became 25–30% shorter due to the elimination of ViewContract.
Migration from MVP to MVVM is a gradual process. 1) Replace ViewContract with LiveData/StateFlow in the Presenter. 2) Remove attach/detach methods — subscription goes through observe(). 3) Rename Presenter to ViewModel. 4) Integrate DI (Hilt/Koin) for ViewModelFactory. Migrating one screen takes 2–4 hours, the entire codebase — 2–4 weeks for a project with 50–100 screens. After migration, ViewContract interfaces are removed, code shrinks, tests remain.
MVP in modern development — the pattern is alive but yields to MVVM and MVI. Google officially recommends MVVM with Jetpack for new projects. Apple — MVVM with SwiftUI. However, knowledge of MVP is mandatory for working with legacy code: hundreds of Android apps on Google Play still run on MVP, including apps of major banks, retailers and transport companies. Understanding MVP is the foundation for mastering MVI and Clean Architecture, as the Presenter is the direct predecessor of Use Case in Robert Martin's terms.
Frequently Asked Questions
In MVP, the Presenter interacts with the View through the ViewContract interface, not directly. In MVC, the Controller has a direct reference to the View through IBOutlet/findViewById. MVP allows testing the Presenter with unit tests without iOS Simulator or Android Emulator, since the Presenter does not depend on UIKit or Android Framework. MVC requires launching the app to test the controller.
MVP is justified in legacy projects already built on this pattern, and in apps without support for reactive mechanisms (LiveData, StateFlow, Combine). For new projects Google recommends MVVM with Jetpack (Android) and Apple recommends MVVM with SwiftUI (iOS). MVP remains the best choice for projects on pure UIKit without Combine when unit testing of business logic is required.
On Android — use a retain fragment (setRetainInstance(true)) or ViewModel from Jetpack. The retain fragment stores the Presenter on rotation and passes it to the new Activity. Google's ViewModel is a modern alternative that automatically preserves state on rotation without retain fragments. On iOS — the Presenter is recreated on each viewDidLoad but is cached in a separate coordinator service.
At least 4: ViewContract interface, ViewContract implementation (Activity/Fragment), Presenter, Model (Repository). If Dagger/Hilt is used, a DI module is added. For 50 screens this is 200+ classes. MVVM reduces the count by 1 file per screen (ViewContract is not needed), MVI adds State and Intent classes. The number of classes is the main argument against MVP in large projects.
Passive View — the View contains no logic, the Presenter fully manages state and data. Supervising Controller — the View itself performs simple binding (data binding), the Presenter intervenes in complex scenarios. In mobile development Passive View dominates — it provides maximum testability and predictability. Supervising Controller is used in web frameworks (ASP.NET Web Forms, GWT).
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also