MVP (Model-View-Presenter) — wzorzec architektoniczny, w którym Presenter pełni rolę pośrednika między Model a View poprzez interfejs ViewContract. W przeciwieństwie do MVC, gdzie Controller bezpośrednio zarządza View przez UIKit, Presenter nie zależy od frameworka — działa poprzez abstrakcję, co czyni go testowalnym bez Android SDK ani UIKit. MVP jest szeroko stosowany w rozwoju Androida przed pojawieniem się Jetpack i pozostaje aktualny dla projektów legacy. Więcej w artykule Martina Fowlera.
Najważniejsze
MVP (Model-View-Presenter) — wzorzec architektoniczny zaproponowany przez Martina Fowlera na początku lat 2000 jako ewolucja MVC w celu poprawy testowalności interfejsu użytkownika. Model zarządza danymi i logiką biznesową, View odpowiada za wyświetlanie i obsługę wejścia użytkownika, Presenter — centralny komponent, który otrzymuje zdarzenia od View, pobiera dane z Model i formułuje stan do wyświetlenia.
Główna różnica między MVP a MVC — Presenter nie ma bezpośredniego odwołania do View. Zamiast tego Presenter współdziała z View poprzez interfejs ViewContract. View implementuje ten interfejs i przekazuje siebie Presenter. To zrywa zależność od UIKit (iOS) lub Android Framework — Presenter jest testowany izolowanie z implementacją mock ViewContract. W MVC kontroler UIViewController bezpośrednio aktualizuje UILabel, w MVP Presenter wywołuje metodę view.showName(name), a View decyduje, jak wyświetlić.
| Komponent | Odpowiedzialność | Testowalność |
|---|---|---|
| Model | Dane, logika biznesowa, wywołania sieciowe | Testy jednostkowe (nie zależy od UI) |
| View | Wyświetlanie UI, przekazywanie zdarzeń Presenter | Implementacja mock poprzez interfejs |
| Presenter | Logika biznesowa, zarządzanie stanem, nawigacja | Testy jednostkowe (przez mock ViewContract) |
Zasada jednej odpowiedzialności w MVP jest przestrzegana ściślej niż w MVC: View odpowiada tylko za renderowanie, Model — za dane, Presenter — za logikę i koordynację. W rzeczywistych projektach Presenter zajmuje 40–60% kodu ekranu, View — 20–30%, Model — 20–30%. Taki podział pozwala testować kluczową logikę biznesową bez uruchamiania emulatora Android lub symulatora iOS.
MVP w Android wykorzystuje Activity lub Fragment jako View, która implementuje ViewContract — interfejs z metodami wyświetlania danych. Presenter jest tworzony w Activity, subskrybuje View do siebie i zarządza ładowaniem danych. Przy obrocie ekranu Activity jest odtwarzane — Presenter może być zachowany przez retain-fragment lub zewnętrzne przechowywanie, co rozwiązuje problem utraty stanu charakterystyczny dla czystego MVC.
// ViewContract — interfejs do komunikacji Presenter z View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — testowalna warstwa logiki
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 ?: "Nieznany błąd")
}
}
}
}
// View (Activity) implementuje interfejs
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) { /* zaktualizować UI */ }
override fun showLoading() { /* pokazać ProgressBar */ }
override fun hideLoading() { /* ukryć ProgressBar */ }
override fun showError(message: String) { /* pokazać Snackbar */ }
}
Zarządzanie cyklem życia — kluczowy problem MVP na Android. Activity jest niszczone przy obrocie ekranu, a presenter.attachView() jest wywoływany ponownie w onCreate(). Jeśli ładowanie danych jest asynchroniczne (RxJava, korutyny), w momencie zakończenia View może być odłączone. Rozwiązanie — anulowanie subskrypcji w detachView() lub użycie Loader z Support Library (dla projektów bez Jetpack). W IT Sectr przez lata stosowaliśmy połączenie MVP + RxJava w projektach komercyjnych — wzorzec jest stabilny, ale wymaga dyscypliny w zarządzaniu subskrypcjami.
Retain-fragmenty — mechanizm zachowania Presenter przy obrocie ekranu. Fragment bez UI (setRetainInstance(true)) żyje dłużej niż Activity i przechowuje referencję do Presenter. Przy odtwarzaniu Activity fragment przekazuje ten sam Presenter nowej Activity. Retain-fragmenty są deprecated od AndroidX, ale ich pre-Jetpack odpowiednik (Fragment.setRetainInstance) wciąż działa w projektach legacy. We współczesnym rozwoju Google zaleca ViewModel zamiast retain-fragmentów.
MVP w iOS jest budowany poprzez protokół View. UIViewController implementuje protokół, Presenter nie importuje UIKit i jest czysto testowalny. W przeciwieństwie do Apple MVC, gdzie UIViewController sam zawiera logikę i bezpośrednie połączenia IBOutlet, Presenter zarządza stanem i wydaje polecenia View poprzez metody protokołu. View nie podejmuje decyzji — wykonuje polecenia Presenter: showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — abstrakcja dla Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — czysta logika, bez 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) implementuje protokół
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
}
// ... pozostałe metody protokołu
}
Weak reference na View — obowiązkowy warunek w iOS MVP. UIViewController może zostać zniszczony (pop z stosu nawigacji), a jego domknięcie w Presenter utworzy retain cycle. Słaba referencja (weak var) gwarantuje, że View jest zwalniana, gdy znika z ekranu, niezależnie od obecności operacji asynchronicznych w Presenter. W Android podobny problem rozwiązuje się przez detachView() — wywołanie w onDestroy() zeruje referencję do View.
Passive View vs Supervising Controller — dwa warianty MVP od Martina Fowlera. Passive View: View nie zawiera logiki, Presenter całkowicie zarządza stanem. Supervising Controller: View sama wykonuje proste wiązanie danych (np. przez data binding), Presenter ingeruje tylko w złożone scenariusze. W programowaniu mobilnym Passive View jest stosowany częściej — zapewnia maksymalną testowalność i przewidywalność stanu ekranu.
Główna różnica między MVP a MVC — sposób komunikacji z View. W MVC Controller ma bezpośrednie odwołanie do View (UIViewController.IBOutlets, Activity.findViewById). W MVP Presenter współdziała z View poprzez interfejs ViewContract. Ta różnica radykalnie zmienia testowalność: Obiekt Mock implementujący ViewContract pozwala sprawdzić logikę Presenter bez uruchamiania aplikacji, emulatora i frameworka UI.
| Kryterium | MVC | MVP |
|---|---|---|
| Połączenie z View | Bezpośrednie (Controller → View) | Przez interfejs (Presenter → ViewContract) |
| Testowanie logiki | Wymaga UIKit/Android Framework | Testy jednostkowe bez zależności platformowych |
| Cykl życia | Controller żyje z ekranem | Presenter może żyć dłużej (retain) |
| Złożoność | Minimalna | +1 interfejs na ekran |
| Massive Controller | Typowy problem | Logika w Presenter, View cienka |
Przykład testu jednostkowego Presenter w Kotlin: tworzony jest mock UserView, przekazywany do Presenter, wywoływany loadUser, sprawdzane, czy showUser został wywołany z poprawnymi danymi. Test wykonuje się w milisekundach, nie wymaga emulatora. W iOS analogicznie — OCMock lub stub protokołu sprawdza wywołania metod UserViewProtocol. W projektach IT Sectr z MVP pokrycie logiki biznesowej testami jednostkowymi osiągało 85–90%, co jest 2–3 razy wyższe niż w podobnych projektach MVC.
Kiedy MVP jest preferowane nad MVC — w projektach z rygorystycznymi wymaganiami dotyczącymi stabilności: aplikacje bankowe, systemy medyczne, terminale płatnicze. W tych obszarach koszt błędu jest wysoki, a testy jednostkowe są krytyczne. W projektach Post-MVP (gdy produkt jest już na rynku, ale baza kodu jest legacy) MVP pozwala stopniowo przenosić logikę z Massive View Controller do testowalnej warstwy bez całkowitego przepisywania architektury.
Główne wady MVP — wzrost liczby interfejsów i ręczne zarządzanie subskrypcjami. Każdy ekran wymaga co najmniej jednego ViewContract + Presenter, dla 50 ekranów — 50 interfejsów i 50 klas Presenter. W MVVM ViewModel zastępuje Presenter i wykorzystuje mechanizmy reaktywne (LiveData, StateFlow, ObservableObject), co eliminuje potrzebę ręcznego attach/detach i interfejsów ViewContract.
RxJava i MVP — popularna kombinacja w Android 2015–2019. Presenter subskrybuje się na Observable z Repository, wyświetla wynik przez ViewContract. Problem: disposable wymaga jawnego anulowania w detachView(), w przeciwnym razie wyciek subskrypcji spowoduje crash przy aktualizacji odłączonego View. Biblioteki RxLifecycle i AutoDispose częściowo zautomatyzowały anulowanie, ale dodawały zależność. W IT Sectr przeszliśmy z MVP+RxJava na MVVM+Flow w 2020 roku — kod stał się krótszy o 25–30% dzięki wyeliminowaniu ViewContract.
Migracja z MVP na MVVM — proces etapowy. 1) Zastąpić ViewContract na LiveData/StateFlow w Presenter. 2) Usunąć metody attach/detach — subskrypcja odbywa się przez observe(). 3) Zmienić nazwę Presenter na ViewModel. 4) Zintegrować DI (Hilt/Koin) dla ViewModelFactory. Migracja jednego ekranu zajmuje 2–4 godziny, całej bazy kodu — 2–4 tygodnie dla projektu na 50–100 ekranów. Po migracji interfejsy ViewContract są usuwane, kod się skraca, testy pozostają.
MVP we współczesnym programowaniu — wzorzec żyje, ale ustępuje MVVM i MVI. Google oficjalnie zaleca MVVM z Jetpack dla nowych projektów. Apple — MVVM ze SwiftUI. Jednak znajomość MVP jest obowiązkowa do pracy z kodem legacy: setki aplikacji Android w Google Play wciąż działają na MVP, w tym aplikacje dużych banków, sieci handlowych i firm transportowych. Zrozumienie MVP to fundament do opanowania MVI i Clean Architecture, ponieważ Presenter jest bezpośrednim poprzednikiem Use Case w terminologii Roberta Martina.
Często zadawane pytania
W MVP Presenter współdziała z View poprzez interfejs ViewContract, a nie bezpośrednio. W MVC Controller ma bezpośrednie odwołanie do View przez IBOutlet/findViewById. MVP pozwala testować Presenter testami jednostkowymi bez iOS Simulator lub Android Emulator, ponieważ Presenter nie zależy od UIKit ani Android Framework. MVC wymaga uruchomienia aplikacji do testowania kontrolera.
MVP jest uzasadniony w projektach legacy już zbudowanych na tym wzorcu oraz w aplikacjach bez wsparcia mechanizmów reaktywnych (LiveData, StateFlow, Combine). Dla nowych projektów Google zaleca MVVM z Jetpack (Android), a Apple zaleca MVVM ze SwiftUI (iOS). MVP pozostaje najlepszym wyborem dla projektów na czystym UIKit bez Combine przy wymaganiu testowania jednostkowego logiki biznesowej.
W Android — użyć retain-fragment (setRetainInstance(true)) lub ViewModel z Jetpack. Retain-fragment przechowuje Presenter przy obrocie i przekazuje go nowej Activity. ViewModel od Google — nowoczesna alternatywa, automatycznie zachowująca stan przy obrocie bez retain-fragmentów. W iOS — Presenter jest tworzony od nowa przy każdym viewDidLoad, ale cachowany w osobnym serwisie koordynatorze.
Minimum 4: interfejs ViewContract, implementacja ViewContract (Activity/Fragment), Presenter, Model (Repository). Jeśli używane są Dagger/Hilt, dodawany jest moduł DI. Dla 50 ekranów to 200+ klas. MVVM zmniejsza liczbę o 1 plik na ekran (ViewContract nie jest potrzebny), MVI dodaje klasy State i Intent. Liczba klas — główny argument przeciw MVP w dużych projektach.
Passive View — View nie zawiera logiki, Presenter całkowicie zarządza stanem i danymi. Supervising Controller — View sama wykonuje proste wiązanie (data binding), Presenter ingeruje w złożone scenariusze. W programowaniu mobilnym dominuje Passive View — zapewnia maksymalną testowalność i przewidywalność. Supervising Controller jest stosowany w frameworkach webowych (ASP.NET Web Forms, GWT).
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również