MVP (Model-View-Presenter) — model arhitectural în care Presenter acționează ca mediator între Model și View prin intermediul interfeței ViewContract. Spre deosebire de MVC, unde Controller gestionează direct View prin UIKit, Presenter nu depinde de framework — lucrează prin abstractizare, ceea ce îl face testabil fără Android SDK sau UIKit. MVP este utilizat pe scară largă în dezvoltarea Android înainte de apariția Jetpack și rămâne relevant pentru proiectele legacy. Mai multe detalii în articolul lui Martin Fowler.
Puncte principale
MVP (Model-View-Presenter) — model arhitectural propus de Martin Fowler la începutul anilor 2000 ca evoluție a MVC pentru îmbunătățirea testabilității interfeței de utilizator. Model gestionează datele și logica de business, View se ocupă de afișare și procesarea intrărilor utilizatorului, Presenter — componenta centrală care primește evenimente de la View, extrage date din Model și formează starea pentru afișare.
Diferența principală dintre MVP și MVC — Presenter nu are o referință directă la View. În schimb, Presenter interacționează cu View prin interfața ViewContract. View implementează această interfață și se transmite pe sine Presenter. Aceasta rupe dependența de UIKit (iOS) sau Android Framework — Presenter este testat izolat cu o implementare mock a ViewContract. În MVC controlerul UIViewController actualizează direct UILabel, în MVP Presenter apelează metoda view.showName(name), iar View decide cum să afișeze.
| Componentă | Responsabilitate | Testabilitate |
|---|---|---|
| Model | Date, logică de business, apeluri de rețea | Teste unitare (nu depinde de UI) |
| View | Afișarea UI, transmiterea evenimentelor la Presenter | Implementare mock prin interfață |
| Presenter | Logică de business, gestionarea stării, navigare | Teste unitare (prin mock ViewContract) |
Principiul responsabilității unice în MVP este respectat mai strict decât în MVC: View răspunde doar de randare, Model — de date, Presenter — de logică și coordonare. În proiectele reale Presenter ocupă 40–60% din codul ecranului, View — 20–30%, Model — 20–30%. Această distribuție permite testarea logicii de business cheie fără a porni emulatorul Android sau simulatorul iOS.
MVP în Android utilizează Activity sau Fragment ca View care implementează ViewContract — o interfață cu metode de afișare a datelor. Presenter este creat în Activity, abonează View la sine și gestionează încărcarea datelor. La rotirea ecranului Activity este recreat — Presenter poate fi păstrat printr-un retain-fragment sau stocare externă, ceea ce rezolvă problema pierderii stării caracteristică MVC-ului pur.
// ViewContract — interfață pentru comunicarea Presenter cu View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — strat de logică testabil
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 ?: "Eroare necunoscută")
}
}
}
}
// View (Activity) implementează interfața
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) { /* actualizează UI */ }
override fun showLoading() { /* afișează ProgressBar */ }
override fun hideLoading() { /* ascunde ProgressBar */ }
override fun showError(message: String) { /* afișează Snackbar */ }
}
Gestionarea ciclului de viață — problema cheie a MVP pe Android. Activity este distrus la rotirea ecranului, iar presenter.attachView() este apelat din nou în onCreate(). Dacă încărcarea datelor este asincronă (RxJava, corutine), la momentul finalizării View poate fi detașată. Soluție — anularea abonamentelor în detachView() sau utilizarea Loader din Support Library (pentru proiecte fără Jetpack). La IT Sectr am folosit ani de zile combinația MVP + RxJava în proiecte comerciale — modelul este stabil, dar necesită disciplină în gestionarea abonamentelor.
Retain-fragment-ele — mecanism de păstrare a Presenter la rotirea ecranului. Fragmentul fără UI (setRetainInstance(true)) trăiește mai mult decât Activity și păstrează referința la Presenter. La recrearea Activity, fragmentul transmite același Presenter noii Activity. Retain-fragment-ele sunt depreciate din AndroidX, dar echivalentul lor pre-Jetpack (Fragment.setRetainInstance) încă funcționează în proiectele legacy. În dezvoltarea modernă Google recomandă ViewModel în locul retain-fragment-elor.
MVP în iOS este construit printr-un protocol View. UIViewController implementează protocolul, Presenter nu importă UIKit și este testat pur. Spre deosebire de Apple MVC, unde UIViewController conține el însuși logica și conexiunile directe IBOutlet, Presenter gestionează starea și comandă View prin metodele protocolului. View nu ia decizii — execută comenzile Presenter: showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — abstractizare pentru Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — logică pură, fără 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) implementează protocolul
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
}
// ... restul metodelor protocolului
}
Weak reference la View — o condiție obligatorie în iOS MVP. UIViewController poate fi distrus (pop din stiva de navigare), iar închiderea sa în Presenter va crea un retain cycle. Referința slabă (weak var) garantează că View este eliberată când părăsește ecranul, indiferent de prezența operațiunilor asincrone în Presenter. În Android o problemă similară se rezolvă prin detachView() — apelul în onDestroy() anulează referința la View.
Passive View vs Supervising Controller — două variante MVP de la Martin Fowler. Passive View: View nu conține logică, Presenter gestionează complet starea. Supervising Controller: View face singură legarea simplă a datelor (de exemplu, prin data binding), Presenter intervine doar în scenarii complexe. În dezvoltarea mobilă Passive View este folosit mai des — oferă testabilitate maximă și predictibilitate a stării ecranului.
Diferența principală dintre MVP și MVC — modul de comunicare cu View. În MVC Controller are o referință directă la View (UIViewController.IBOutlets, Activity.findViewById). În MVP Presenter interacționează cu View prin interfața ViewContract. Această diferență schimbă radical testabilitatea: un obiect Mock care implementează ViewContract permite verificarea logicii Presenter fără a porni aplicația, emulatorul și framework-ul UI.
| Criteriu | MVC | MVP |
|---|---|---|
| Legătura cu View | Directă (Controller → View) | Prin interfață (Presenter → ViewContract) |
| Testarea logicii | Necesită UIKit/Android Framework | Teste unitare fără dependențe de platformă |
| Ciclul de viață | Controller trăiește cu ecranul | Presenter poate trăi mai mult (retain) |
| Complexitate | Minimă | +1 interfață per ecran |
| Massive Controller | Problemă tipică | Logica în Presenter, View subțire |
Exemplu de test unitar Presenter în Kotlin: se creează un mock UserView, se transmite Presenter, se apelează loadUser, se verifică că showUser a fost apelat cu date corecte. Testul se execută în milisecunde, nu necesită emulator. Pe iOS similar — OCMock sau un stub de protocol verifică apelurile metodelor UserViewProtocol. În proiectele IT Sectr cu MVP, acoperirea logicii de business cu teste unitare ajungea la 85–90%, de 2–3 ori mai mult decât în proiecte MVC similare.
Când MVP este preferabil MVC — în proiecte cu cerințe stricte de stabilitate: aplicații bancare, sisteme medicale, terminale de plată. În aceste domenii costul erorii este ridicat, iar testele unitare sunt critice. În proiectele Post-MVP (când produsul este deja pe piață, dar baza de cod este legacy) MVP permite mutarea treptată a logicii din Massive View Controller într-un strat testabil fără rescrierea completă a arhitecturii.
Principalele dezavantaje ale MVP — creșterea numărului de interfețe și gestionarea manuală a abonamentelor. Fiecare ecran necesită cel puțin un ViewContract + Presenter, pentru 50 de ecrane — 50 de interfețe și 50 de clase Presenter. În MVVM ViewModel înlocuiește Presenter și utilizează mecanisme reactive (LiveData, StateFlow, ObservableObject), ceea ce elimină necesitatea attach/detach manual și a interfețelor ViewContract.
RxJava și MVP — combinație populară în Android 2015–2019. Presenter se abonează la Observable din Repository, afișează rezultatul prin ViewContract. Problemă: disposable trebuie anulat explicit în detachView(), altfel scurgerea abonamentului va provoca un crash la actualizarea View detașate. Bibliotecile RxLifecycle și AutoDispose au automatizat parțial dezabonarea, dar au adăugat dependență. La IT Sectr am trecut de la MVP+RxJava la MVVM+Flow în 2020 — codul s-a scurtat cu 25–30% datorită eliminării ViewContract.
Migrarea de la MVP la MVVM — proces în etape. 1) Înlocuirea ViewContract cu LiveData/StateFlow în Presenter. 2) Eliminarea metodelor attach/detach — abonarea se face prin observe(). 3) Redenumirea Presenter în ViewModel. 4) Integrarea DI (Hilt/Koin) pentru ViewModelFactory. Migrarea unui ecran durează 2–4 ore, a întregii baze de cod — 2–4 săptămâni pentru un proiect de 50–100 de ecrane. După migrare interfețele ViewContract sunt șterse, codul se scurtează, testele rămân.
MVP în dezvoltarea modernă — modelul trăiește, dar cedează în fața MVVM și MVI. Google recomandă oficial MVVM cu Jetpack pentru proiecte noi. Apple — MVVM cu SwiftUI. Cu toate acestea, cunoașterea MVP este obligatorie pentru lucrul cu cod legacy: sute de aplicații Android în Google Play încă funcționează pe MVP, inclusiv aplicații ale marilor bănci, retaileri și companii de transport. Înțelegerea MVP este fundamentul pentru stăpânirea MVI și Clean Architecture, deoarece Presenter este predecesorul direct al Use Case în terminologia lui Robert Martin.
Întrebări frecvente
În MVP Presenter interacționează cu View prin interfața ViewContract, nu direct. În MVC Controller are o referință directă la View prin IBOutlet/findViewById. MVP permite testarea Presenter cu teste unitare fără iOS Simulator sau Android Emulator, deoarece Presenter nu depinde de UIKit sau Android Framework. MVC necesită pornirea aplicației pentru testarea controlerului.
MVP este justificat în proiectele legacy deja construite pe acest model și în aplicațiile fără suport pentru mecanisme reactive (LiveData, StateFlow, Combine). Pentru proiecte noi Google recomandă MVVM cu Jetpack (Android), iar Apple recomandă MVVM cu SwiftUI (iOS). MVP rămâne cea mai bună alegere pentru proiecte pe UIKit pur fără Combine când se cere testarea unitară a logicii de business.
Pe Android — folosind retain-fragment (setRetainInstance(true)) sau ViewModel din Jetpack. Retain-fragment păstrează Presenter la rotire și îl transmite noii Activity. ViewModel de la Google — o alternativă modernă care păstrează automat starea la rotire fără retain-fragment-e. Pe iOS — Presenter este creat din nou la fiecare viewDidLoad, dar este cache-uit într-un serviciu coordinator separat.
Minimum 4: interfața ViewContract, implementarea ViewContract (Activity/Fragment), Presenter, Model (Repository). Dacă se folosesc Dagger/Hilt, se adaugă un modul DI. Pentru 50 de ecrane sunt 200+ clase. MVVM reduce numărul cu 1 fișier per ecran (ViewContract nu e necesar), MVI adaugă clasele State și Intent. Numărul de clase — principalul argument împotriva MVP în proiecte mari.
Passive View — View nu conține logică, Presenter gestionează complet starea și datele. Supervising Controller — View face singură legarea simplă (data binding), Presenter intervine în scenarii complexe. În dezvoltarea mobilă domină Passive View — oferă testabilitate maximă și predictibilitate. Supervising Controller este folosit în framework-uri web (ASP.NET Web Forms, GWT).
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și