MVP (Model-View-Presenter) — un pattern architetturale in cui il Presenter agisce da intermediario tra il Model e la View attraverso l'interfaccia ViewContract. A differenza di MVC, dove il Controller gestisce direttamente la View tramite UIKit, il Presenter non dipende dal framework — lavora attraverso l'astrazione, rendendolo testabile senza Android SDK o UIKit. MVP è ampiamente usato nello sviluppo Android prima di Jetpack e rimane rilevante per progetti legacy. Maggiori informazioni nell'articolo di Martin Fowler.
Punti chiave
MVP (Model-View-Presenter) — un pattern architetturale proposto da Martin Fowler all'inizio degli anni 2000 come evoluzione di MVC per migliorare la testabilità dell'interfaccia utente. Il Model gestisce dati e logica di business, la View è responsabile del rendering e dell'elaborazione dell'input utente, il Presenter è il componente centrale che riceve eventi dalla View, recupera dati dal Model e forma lo stato per la visualizzazione.
La principale differenza tra MVP e MVC — il Presenter non ha un riferimento diretto alla View. Invece, il Presenter interagisce con la View attraverso l'interfaccia ViewContract. La View implementa questa interfaccia e si passa al Presenter. Questo rompe la dipendenza da UIKit (iOS) o Android Framework — il Presenter può essere testato isolatamente con un'implementazione mock di ViewContract. In MVC il controller UIViewController aggiorna direttamente UILabel, in MVP il Presenter chiama view.showName(name), e la View decide come visualizzare.
| Componente | Responsabilità | Testabilità |
|---|---|---|
| Model | Dati, logica di business, chiamate di rete | Test unitari (indipendente da UI) |
| View | Rendering UI, passaggio eventi al Presenter | Implementazione mock tramite interfaccia |
| Presenter | Logica di business, gestione stato, navigazione | Test unitari (tramite mock ViewContract) |
Principio di Responsabilità Unica in MVP è seguito più rigorosamente che in MVC: la View è responsabile solo del rendering, il Model dei dati, il Presenter della logica e del coordinamento. Nei progetti reali il Presenter occupa il 40–60% del codice dello schermo, la View — 20–30%, il Model — 20–30%. Questa distribuzione permette di testare la logica di business chiave senza avviare un emulatore Android o simulatore iOS.
MVP in Android usa Activity o Fragment come View, che implementa ViewContract — un'interfaccia con metodi di visualizzazione dati. Il Presenter viene creato nell'Activity, attacca la View a sé e gestisce il caricamento dei dati. Quando lo schermo ruota, l'Activity viene ricreata — il Presenter può essere preservato tramite un retain fragment o archiviazione esterna, risolvendo il problema di perdita di stato caratteristico del MVC puro.
// ViewContract — interfaccia per collegare Presenter con View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — livello di logica testabile
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) implementa l'interfaccia
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) { /* aggiornare la UI */ }
override fun showLoading() { /* mostrare ProgressBar */ }
override fun hideLoading() { /* nascondere ProgressBar */ }
override fun showError(message: String) { /* mostrare Snackbar */ }
}
Gestione del ciclo di vita — un problema chiave di MVP su Android. L'Activity viene distrutta alla rotazione dello schermo, e presenter.attachView() viene richiamato in onCreate(). Se il caricamento dei dati è asincrono (RxJava, coroutine), al momento del completamento la View potrebbe essere detached. La soluzione — annullare le sottoscrizioni in detachView() o usare il Loader della Support Library (per progetti senza Jetpack). In IT Sectr abbiamo usato la combinazione MVP + RxJava per anni in progetti commerciali — il pattern è stabile ma richiede disciplina nella gestione delle sottoscrizioni.
Retain fragment — un meccanismo per preservare il Presenter alla rotazione dello schermo. Un fragment senza UI (setRetainInstance(true)) sopravvive all'Activity e mantiene un riferimento al Presenter. Quando l'Activity viene ricreata, il fragment passa lo stesso Presenter alla nuova Activity. I retain fragment sono deprecati da AndroidX, ma il loro analogo pre-Jetpack (Fragment.setRetainInstance) funziona ancora in progetti legacy. Nello sviluppo moderno Google raccomanda ViewModel invece dei retain fragment.
MVP in iOS è costruito attraverso un protocollo View. UIViewController implementa il protocollo, il Presenter non importa UIKit ed è puramente testabile. A differenza di Apple MVC, dove UIViewController contiene logica e connessioni IBOutlet dirette, il Presenter gestisce lo stato e comanda la View attraverso metodi del protocollo. La View non prende decisioni — esegue i comandi del Presenter: showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — astrazione per Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — logica pura, senza 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) implementa il protocollo
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
}
// ... restanti metodi del protocollo
}
Weak reference sulla View — obbligatoria in MVP iOS. Un UIViewController può essere distrutto (pop dalla navigation stack), e la sua chiusura nel Presenter creerebbe un retain cycle. Un riferimento debole (weak var) garantisce che la View venga rilasciata quando esce dallo schermo, indipendentemente dalle operazioni asincrone nel Presenter. In Android un problema simile viene risolto tramite detachView() — chiamarlo in onDestroy() annulla il riferimento alla View.
Passive View vs Supervising Controller — due varianti di MVP di Martin Fowler. Passive View: la View non contiene logica, il Presenter gestisce completamente lo stato. Supervising Controller: la View stessa fa un semplice data binding (ad esempio, tramite data binding), il Presenter interviene solo in scenari complessi. Nello sviluppo mobile, Passive View è usato più spesso — offre massima testabilità e prevedibilità dello stato dello schermo.
La principale differenza tra MVP e MVC è il modo di comunicare con la View. In MVC il Controller ha un riferimento diretto alla View (UIViewController.IBOutlets, Activity.findViewById). In MVP il Presenter interagisce con la View attraverso l'interfaccia ViewContract. Questa differenza cambia fondamentalmente la testabilità: un oggetto mock che implementa ViewContract permette di testare la logica del Presenter senza avviare l'app, l'emulatore o il framework UI.
| Criterio | MVC | MVP |
|---|---|---|
| Connessione con View | Diretta (Controller → View) | Tramite interfaccia (Presenter → ViewContract) |
| Test della logica | Richiede UIKit/Android Framework | Test unitari senza dipendenze di piattaforma |
| Ciclo di vita | Controller vive con lo schermo | Presenter può sopravvivere (retain) |
| Complessità | Minima | +1 interfaccia per schermo |
| Massive Controller | Problema tipico | Logica nel Presenter, View sottile |
Esempio di test unitario del Presenter in Kotlin: viene creato un mock UserView, passato al Presenter, viene chiamato loadUser, si verifica che showUser sia stato chiamato con dati corretti. Il test viene eseguito in millisecondi, nessun emulatore richiesto. Su iOS similmente — OCMock o uno stub di protocollo verifica le chiamate ai metodi di UserViewProtocol. Nei progetti IT Sectr con MVP, la copertura dei test unitari della logica di business raggiungeva l'85–90%, 2–3 volte superiore rispetto a progetti MVC simili.
Quando MVP è preferibile a MVC — in progetti con severi requisiti di stabilità: applicazioni bancarie, sistemi medici, terminali di pagamento. In questi ambiti il costo di un errore è alto e i test unitari sono critici. Nei progetti post-MVP (quando il prodotto è già sul mercato ma la base di codice è legacy), MVP permette di estrarre gradualmente la logica dal Massive View Controller in un livello testabile senza una riscrittura architetturale completa.
I principali svantaggi di MVP — la crescita del numero di interfacce e la gestione manuale delle sottoscrizioni. Ogni schermo richiede almeno un ViewContract + Presenter, per 50 schermi — 50 interfacce e 50 classi Presenter. In MVVM, ViewModel sostituisce il Presenter e utilizza meccanismi reattivi (LiveData, StateFlow, ObservableObject), eliminando la necessità di attach/detach manuali e interfacce ViewContract.
RxJava e MVP — una combinazione popolare in Android 2015–2019. Il Presenter si sottoscrive a un Observable dal Repository, mostra il risultato tramite ViewContract. Il problema: disposable deve essere cancellato esplicitamente in detachView(), altrimenti una perdita di sottoscrizione causerà un crash durante l'aggiornamento di una View detached. Le librerie RxLifecycle e AutoDispose hanno parzialmente automatizzato la cancellazione ma hanno aggiunto dipendenze. In IT Sectr siamo passati da MVP+RxJava a MVVM+Flow nel 2020 — il codice è diventato più corto del 25–30% grazie all'eliminazione di ViewContract.
Migrazione da MVP a MVVM — un processo graduale. 1) Sostituire ViewContract con LiveData/StateFlow nel Presenter. 2) Rimuovere i metodi attach/detach — la sottoscrizione avviene tramite observe(). 3) Rinominare Presenter in ViewModel. 4) Integrare DI (Hilt/Koin) per ViewModelFactory. La migrazione di uno schermo richiede 2–4 ore, dell'intera base di codice — 2–4 settimane per un progetto con 50–100 schermi. Dopo la migrazione, le interfacce ViewContract vengono rimosse, il codice si riduce, i test rimangono.
MVP nello sviluppo moderno — il pattern è vivo ma cede il passo a MVVM e MVI. Google raccomanda ufficialmente MVVM con Jetpack per i nuovi progetti. Apple — MVVM con SwiftUI. Tuttavia, la conoscenza di MVP è obbligatoria per lavorare con codice legacy: centinaia di app Android su Google Play funzionano ancora con MVP, incluse app di grandi banche, rivenditori e aziende di trasporto. Capire MVP è il fondamento per padroneggiare MVI e Clean Architecture, poiché il Presenter è il diretto predecessore di Use Case nei termini di Robert Martin.
Domande frequenti
In MVP, il Presenter interagisce con la View attraverso l'interfaccia ViewContract, non direttamente. In MVC, il Controller ha un riferimento diretto alla View tramite IBOutlet/findViewById. MVP permette di testare il Presenter con test unitari senza iOS Simulator o Android Emulator, poiché il Presenter non dipende da UIKit o Android Framework. MVC richiede l'avvio dell'app per testare il controller.
MVP è giustificato in progetti legacy già costruiti su questo pattern e in app senza supporto di meccanismi reattivi (LiveData, StateFlow, Combine). Per i nuovi progetti, Google raccomanda MVVM con Jetpack (Android) e Apple raccomanda MVVM con SwiftUI (iOS). MVP rimane la scelta migliore per progetti su UIKit puro senza Combine quando sono richiesti test unitari della logica di business.
Su Android — utilizzare un retain fragment (setRetainInstance(true)) o ViewModel di Jetpack. Il retain fragment conserva il Presenter alla rotazione e lo passa alla nuova Activity. Il ViewModel di Google è un'alternativa moderna che preserva automaticamente lo stato alla rotazione senza retain fragment. Su iOS — il Presenter viene ricreato ad ogni viewDidLoad ma viene memorizzato nella cache in un servizio coordinatore separato.
Almeno 4: interfaccia ViewContract, implementazione ViewContract (Activity/Fragment), Presenter, Model (Repository). Se si usa Dagger/Hilt, viene aggiunto un modulo DI. Per 50 schermi sono 200+ classi. MVVM riduce il numero di 1 file per schermo (ViewContract non è necessario), MVI aggiunge classi State e Intent. Il numero di classi è il principale argomento contro MVP nei grandi progetti.
Passive View — la View non contiene logica, il Presenter gestisce completamente stato e dati. Supervising Controller — la View stessa fa un semplice binding (data binding), il Presenter interviene solo in scenari complessi. Nello sviluppo mobile, Passive View domina — offre massima testabilità e prevedibilità. Supervising Controller è usato nei framework web (ASP.NET Web Forms, GWT).
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche