MVP — co to jest, wzorzec Model-View-Presenter w iOS i Android

Autor: IT Sectr Opublikowano: 2026-02-16 Czas czytania: 10 min

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 — trzy komponenty: Model (dane), View (interfejs), Presenter (logika i stan)
  • ViewContract — interfejs, przez który Presenter komunikuje się z View, zapewniając testowalność
  • Presenter — zawiera całą logikę biznesową, nie zależy od klas platformowych Android/iOS
  • Passive View — View jest maksymalnie pasywna, tylko wyświetla dane na polecenia Presenter
  • MVP vs MVC — Presenter jest testowany testami jednostkowymi, Controller w MVC zależy od UIKit/Android Framework

Co to jest MVP: istota wzorca Model-View-Presenter

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ć.

KomponentOdpowiedzialnośćTestowalność
ModelDane, logika biznesowa, wywołania siecioweTesty jednostkowe (nie zależy od UI)
ViewWyświetlanie UI, przekazywanie zdarzeń PresenterImplementacja mock poprzez interfejs
PresenterLogika biznesowa, zarządzanie stanem, nawigacjaTesty 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: Presenter, ViewContract i Activity

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.

kotlin
// 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: Presenter i View Protocol

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.

swift
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.

Różnice między MVP a MVC i zalety testowania

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.

KryteriumMVCMVP
Połączenie z ViewBezpośrednie (Controller → View)Przez interfejs (Presenter → ViewContract)
Testowanie logikiWymaga UIKit/Android FrameworkTesty jednostkowe bez zależności platformowych
Cykl życiaController żyje z ekranemPresenter może żyć dłużej (retain)
ZłożonośćMinimalna+1 interfejs na ekran
Massive ControllerTypowy problemLogika 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.

Ograniczenia MVP i przejście do MVVM

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

Czym różni się MVP od MVC?

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.

Kiedy warto używać MVP zamiast MVVM?

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.

Jak rozwiązać problem utraty Presenter przy obrocie ekranu?

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.

Ile klas potrzeba na jeden ekran w MVP?

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.

Jaka jest różnica między Passive View a Supervising Controller w MVP?

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

  • MVP (Model-View-Presenter) — ewolucja MVC z testowalną warstwą Presenter przez interfejs ViewContract
  • ViewContract — interfejs abstrahujący View od Presenter, umożliwiający testowanie mock
  • Passive View — dominujący wariant MVP w programowaniu mobilnym z pasywnym View
  • Presenter — zawiera logikę biznesową, nie zależy od UIKit ani Android Framework
  • MVP vs MVC — MVP rozwiązuje problem testowania, ale dodaje 1 interfejs na ekran
  • Android retain-fragmenty — zachowanie Presenter przy obrocie ekranu przed pojawieniem się Jetpack ViewModel
  • Migracja na MVVM — zastąpienie ViewContract na LiveData/StateFlow skraca kod o 25–30%

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.

Omów projekt

Przeczytaj również