MVP (Model-View-Presenter) — architekturális minta, amelyben a Presenter közvetítőként működik a Model és a View között a ViewContract interfészen keresztül. Ellentétben az MVC-vel, ahol a Controller közvetlenül irányítja a View-t az UIKit-en keresztül, a Presenter nem függ a keretrendszertől — absztrakción keresztül működik, ami Android SDK vagy UIKit nélkül tesztelhetővé teszi. Az MVP széles körben használták az Android fejlesztésben a Jetpack megjelenése előtt, és továbbra is releváns a legacy projektek számára. Bővebben Martin Fowler cikkében.
Főbb pontok
MVP (Model-View-Presenter) — architekturális minta, amelyet Martin Fowler javasolt a 2000-es évek elején az MVC evolúciójaként a felhasználói felület tesztelhetőségének javítására. A Model kezeli az adatokat és az üzleti logikát, a View felelős a megjelenítésért és a felhasználói bevitel feldolgozásáért, a Presenter — központi komponens, amely eseményeket fogad a View-tól, adatokat nyer ki a Model-ből, és állapotot képez a megjelenítéshez.
Az MVP fő különbsége az MVC-től — a Presenter nem rendelkezik közvetlen hivatkozással a View-ra. Ehelyett a Presenter a ViewContract interfészen keresztül kommunikál a View-val. A View implementálja ezt az interfészt, és átadja magát a Presenter-nek. Ez megszakítja a függőséget az UIKit-től (iOS) vagy az Android Framework-től — a Presenter elkülönítetten tesztelhető a ViewContract mock implementációjával. Az MVC-ben a controller UIViewController közvetlenül frissíti az UILabel-t, az MVP-ben a Presenter meghívja a view.showName(name) metódust, és a View dönti el, hogyan jelenítse meg.
| Komponens | Felelősség | Tesztelhetőség |
|---|---|---|
| Model | Adatok, üzleti logika, hálózati hívások | Unit tesztek (nem függ UI-tól) |
| View | UI megjelenítése, események továbbítása a Presenter-nek | Mock implementáció interfészen keresztül |
| Presenter | Üzleti logika, állapotkezelés, navigáció | Unit tesztek (ViewContract mock-on keresztül) |
Az egyetlen felelősség elve az MVP-ben szigorúbban érvényesül, mint az MVC-ben: a View csak a renderelésért felelős, a Model — az adatokért, a Presenter — a logikáért és koordinációért. Valós projektekben a Presenter a képernyő kódjának 40–60%-át foglalja el, a View — 20–30%-át, a Model — 20–30%-át. Ez a felosztás lehetővé teszi a kulcsfontosságú üzleti logika tesztelését az Android emulátor vagy iOS szimulátor elindítása nélkül.
MVP Android-ban az Activity vagy Fragment View-ként működik, amely implementálja a ViewContract-ot — egy interfészt adatmegjelenítési metódusokkal. A Presenter az Activity-ben jön létre, feliratkoztatja a View-t magára, és kezeli az adatok betöltését. Képernyő elforgatásakor az Activity újra létrejön — a Presenter megőrizhető retain-fragment vagy külső tárolás segítségével, ami megoldja a tiszta MVC-re jellemző állapotvesztés problémáját.
// ViewContract — interfész a Presenter és View közötti kommunikációhoz
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — tesztelhető logikai réteg
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 ?: "Ismeretlen hiba")
}
}
}
}
// View (Activity) implementálja az interfészt
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) { /* a UI frissítése */ }
override fun showLoading() { /* ProgressBar megjelenítése */ }
override fun hideLoading() { /* ProgressBar elrejtése */ }
override fun showError(message: String) { /* Snackbar megjelenítése */ }
}
Életciklus kezelés — az MVP fő problémája Android-on. Az Activity megsemmisül a képernyő elforgatásakor, és a presenter.attachView() újra meghívódik az onCreate()-ben. Ha az adatbetöltés aszinkron (RxJava, korutinok), a befejezés pillanatában a View már le lehet választva. Megoldás — a feliratkozások törlése a detachView()-ben vagy Loader használata a Support Library-ből (Jetpack nélküli projektekhez). Az IT Sectr-nél évekig használtuk az MVP + RxJava kombinációt kereskedelmi projektekben — a minta stabil, de fegyelmet igényel a feliratkozások kezelésében.
Retain-fragmentek — mechanizmus a Presenter megőrzésére képernyő elforgatásakor. A UI nélküli fragment (setRetainInstance(true)) tovább él, mint az Activity, és tárolja a Presenter-re mutató hivatkozást. Az Activity újralétrehozásakor a fragment ugyanazt a Presenter-t adja át az új Activity-nek. A retain-fragmentek elavultak az AndroidX óta, de a pre-Jetpack megfelelőjük (Fragment.setRetainInstance) még mindig működik legacy projektekben. A modern fejlesztésben a Google a ViewModel-t ajánlja a retain-fragmentek helyett.
MVP iOS-ben a View protokollon keresztül épül fel. Az UIViewController implementálja a protokollt, a Presenter nem importálja az UIKit-et, és tisztán tesztelhető. Ellentétben az Apple MVC-vel, ahol az UIViewController maga tartalmazza a logikát és a közvetlen IBOutlet kapcsolatokat, a Presenter kezeli az állapotot és utasítja a View-t a protokoll metódusain keresztül. A View nem hoz döntéseket — végrehajtja a Presenter utasításait: showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — absztrakció a Presenter számára
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — tiszta logika, UIKit nélkül
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) implementálja a protokollt
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
}
// ... a protokoll többi metódusa
}
Weak reference a View-ra — kötelező feltétel az iOS MVP-ben. Az UIViewController megsemmisülhet (pop a navigációs veremből), és a benne lévő closure a Presenter-ben retain cycle-t hoz létre. A gyenge hivatkozás (weak var) garantálja, hogy a View felszabadul, amikor elhagyja a képernyőt, függetlenül az aszinkron műveletek jelenlététől a Presenter-ben. Android-ban hasonló probléma a detachView()-on keresztül oldható meg — az onDestroy()-ben történő hívás nullázza a View-ra mutató hivatkozást.
Passive View vs Supervising Controller — az MVP két változata Martin Fowler-től. Passive View: a View nem tartalmaz logikát, a Presenter teljes mértékben kezeli az állapotot. Supervising Controller: a View maga végez egyszerű adatkötést (pl. data binding segítségével), a Presenter csak összetett forgatókönyvekbe avatkozik be. A mobilfejlesztésben a Passive View-t használják gyakrabban — maximális tesztelhetőséget és a képernyő állapotának kiszámíthatóságát biztosítja.
A fő különbség az MVP és MVC között — a View-val való kommunikáció módja. Az MVC-ben a Controller közvetlen hivatkozással rendelkezik a View-ra (UIViewController.IBOutlets, Activity.findViewById). Az MVP-ben a Presenter a ViewContract interfészen keresztül kommunikál a View-val. Ez a különbség gyökeresen megváltoztatja a tesztelhetőséget: egy Mock objektum, amely implementálja a ViewContract-ot, lehetővé teszi a Presenter logikájának ellenőrzését az alkalmazás, emulátor és UI keretrendszer elindítása nélkül.
| Szempont | MVC | MVP |
|---|---|---|
| Kapcsolat a View-val | Közvetlen (Controller → View) | Interfészen keresztül (Presenter → ViewContract) |
| Logika tesztelése | UIKit/Android Framework szükséges | Unit tesztek platformfüggőségek nélkül |
| Életciklus | Controller a képernyővel él | Presenter tovább élhet (retain) |
| Komplexitás | Minimális | +1 interfész képernyőnként |
| Massive Controller | Tipikus probléma | Logika a Presenter-ben, View vékony |
Presenter unit teszt példa Kotlin-ban: létrehozunk egy mock UserView-t, átadjuk a Presenter-nek, meghívjuk a loadUser-t, ellenőrizzük, hogy a showUser a megfelelő adatokkal lett-e meghívva. A teszt ezredmásodpercek alatt fut le, nem igényel emulátort. iOS-en hasonlóan — az OCMock vagy protokoll stub ellenőrzi a UserViewProtocol metódushívásait. Az IT Sectr MVP-s projektjeiben az üzleti logika unit teszt lefedettsége elérte a 85–90%-ot, ami 2–3-szor magasabb, mint a hasonló MVC projektekben.
Mikor előnyösebb az MVP az MVC-nél — szigorú stabilitási követelményekkel rendelkező projektekben: banki alkalmazások, orvosi rendszerek, fizetési terminálok. Ezekben a területeken a hiba ára magas, és a unit tesztek kritikusak. Post-MVP projektekben (amikor a termék már a piacon van, de a kódbázis legacy) az MVP lehetővé teszi a logika fokozatos kiemelését a Massive View Controller-ből egy tesztelhető rétegbe anélkül, hogy teljesen át kellene írni az architektúrát.
Az MVP fő hátrányai — az interfészek számának növekedése és a feliratkozások kézi kezelése. Minden képernyő legalább egy ViewContract + Presenter párost igényel, 50 képernyőhöz — 50 interfész és 50 Presenter osztály. Az MVVM-ben a ViewModel helyettesíti a Presenter-t, és reaktív mechanizmusokat használ (LiveData, StateFlow, ObservableObject), ami kiküszöböli a kézi attach/detach és a ViewContract interfészek szükségességét.
RxJava és MVP — népszerű kombináció Android 2015–2019. A Presenter feliratkozik az Observable-re a Repository-ból, és megjeleníti az eredményt a ViewContract-on keresztül. Probléma: a disposable-t explicit módon törölni kell a detachView()-ben, különben a feliratkozás szivárgása crash-t okoz a leválasztott View frissítésekor. Az RxLifecycle és AutoDispose könyvtárak részben automatizálták a törlést, de függőséget adtak hozzá. Az IT Sectr-nél 2020-ban váltottunk MVP+RxJava-ról MVVM+Flow-ra — a kód 25–30%-kal rövidebb lett a ViewContract eltűnésének köszönhetően.
Migráció MVP-ről MVVM-re — szakaszos folyamat. 1) A ViewContract cseréje LiveData/StateFlow-ra a Presenter-ben. 2) Az attach/detach metódusok eltávolítása — a feliratkozás az observe()-on keresztül történik. 3) A Presenter átnevezése ViewModel-re. 4) DI (Hilt/Koin) integrálása a ViewModelFactory-hoz. Egy képernyő migrációja 2–4 órát vesz igénybe, a teljes kódbázisé — 2–4 hetet egy 50–100 képernyős projekt esetén. A migráció után a ViewContract interfészek eltávolításra kerülnek, a kód rövidül, a tesztek megmaradnak.
Az MVP a modern fejlesztésben — a minta él, de alulmarad az MVVM-mel és MVI-vel szemben. A Google hivatalosan az MVVM-et ajánlja Jetpack-pel új projektekhez. Az Apple — az MVVM-et SwiftUI-val. Az MVP ismerete azonban kötelező a legacy kóddal való munkához: több száz Android alkalmazás a Google Play-ben még mindig MVP-n fut, beleértve a nagy bankok, kiskereskedők és szállítási vállalatok alkalmazásait. Az MVP megértése az alapja az MVI és Clean Architecture elsajátításának, mivel a Presenter a Use Case közvetlen elődje Robert Martin terminológiájában.
Gyakran Ismételt Kérdések
Az MVP-ben a Presenter a ViewContract interfészen keresztül kommunikál a View-val, nem közvetlenül. Az MVC-ben a Controller közvetlen hivatkozással rendelkezik a View-ra az IBOutlet/findViewById segítségével. Az MVP lehetővé teszi a Presenter unit tesztekkel való tesztelését iOS Simulator vagy Android Emulator nélkül, mivel a Presenter nem függ az UIKit-től vagy Android Framework-től. Az MVC megköveteli az alkalmazás elindítását a controller teszteléséhez.
Az MVP indokolt legacy projektekben, amelyek már erre a mintára épültek, és olyan alkalmazásokban, amelyek nem támogatják a reaktív mechanizmusokat (LiveData, StateFlow, Combine). Új projektekhez a Google az MVVM-et ajánlja Jetpack-pel (Android), az Apple pedig az MVVM-et SwiftUI-val (iOS). Az MVP továbbra is a legjobb választás tiszta UIKit-es projektekhez Combine nélkül, amikor az üzleti logika unit tesztelése szükséges.
Android-on — használjunk retain-fragmentet (setRetainInstance(true)) vagy ViewModel-t a Jetpack-ből. A retain-fragment megőrzi a Presenter-t elforgatáskor, és átadja az új Activity-nek. A Google ViewModel-je — modern alternatíva, amely automatikusan megőrzi az állapotot elforgatáskor retain-fragmentek nélkül. iOS-en — a Presenter minden viewDidLoad-kor újra létrejön, de egy külön koordinátor szolgáltatásban cache-elődik.
Minimum 4: a ViewContract interfész, a ViewContract implementációja (Activity/Fragment), Presenter, Model (Repository). Ha Dagger/Hilt-et használunk, egy DI modul is hozzáadódik. 50 képernyőhöz ez 200+ osztály. Az MVVM képernyőnként 1 fájllal csökkenti a számot (ViewContract nem szükséges), az MVI State és Intent osztályokat ad hozzá. Az osztályok száma — a fő érv az MVP ellen nagy projektekben.
Passive View — a View nem tartalmaz logikát, a Presenter teljes mértékben kezeli az állapotot és az adatokat. Supervising Controller — a View maga végzi az egyszerű kötést (data binding), a Presenter összetett forgatókönyvekbe avatkozik be. A mobilfejlesztésben a Passive View dominál — maximális tesztelhetőséget és kiszámíthatóságot biztosít. A Supervising Controller-t webes keretrendszerekben (ASP.NET Web Forms, GWT) használják.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is