MVP — wat is het, het Model-View-Presenter patroon in iOS en Android

Auteur: IT Sectr Gepubliceerd: 2026-02-16 Leestijd: 10 min

MVP (Model-View-Presenter) — een architectuurpatroon waarbij Presenter fungeert als bemiddelaar tussen Model en View via de ViewContract-interface. In tegenstelling tot MVC, waar Controller direct de View beheert via UIKit, is Presenter niet afhankelijk van een framework — het werkt via abstractie, wat het testbaar maakt zonder Android SDK of UIKit. MVP werd veel gebruikt in Android-ontwikkeling vóór Jetpack en blijft relevant voor legacy-projecten. Meer in het artikel van Martin Fowler.

Belangrijkste punten

  • MVP — drie componenten: Model (gegevens), View (interface), Presenter (logica en toestand)
  • ViewContract — de interface waarmee Presenter communiceert met View, wat testbaarheid garandeert
  • Presenter — bevat alle bedrijfslogica, is niet afhankelijk van platformklassen Android/iOS
  • Passive View — View is maximaal passief, toont alleen gegevens op commando van Presenter
  • MVP vs MVC — Presenter wordt getest met unittesten, Controller in MVC is afhankelijk van UIKit/Android Framework

Wat is MVP: essentie van het Model-View-Presenter patroon

MVP (Model-View-Presenter) — een architectuurpatroon voorgesteld door Martin Fowler in het begin van de jaren 2000 als een evolutie van MVC voor het verbeteren van de testbaarheid van de gebruikersinterface. Model beheert gegevens en bedrijfslogica, View is verantwoordelijk voor weergave en verwerking van gebruikersinvoer, Presenter — de centrale component die gebeurtenissen van View ontvangt, gegevens uit Model haalt en de toestand voor weergave vormt.

Het belangrijkste verschil tussen MVP en MVC — Presenter heeft geen directe verwijzing naar View. In plaats daarvan communiceert Presenter met View via de ViewContract-interface. View implementeert deze interface en geeft zichzelf door aan Presenter. Dit verbreekt de afhankelijkheid van UIKit (iOS) of Android Framework — Presenter wordt geïsoleerd getest met een mock-implementatie van ViewContract. In MVC werkt de controller UIViewController direct UILabel bij, in MVP roept Presenter de methode view.showName(name) aan en View beslist hoe weer te geven.

ComponentVerantwoordelijkheidTestbaarheid
ModelGegevens, bedrijfslogica, netwerkaanroepenUnittesten (onafhankelijk van UI)
ViewWeergave van UI, doorgeven van gebeurtenissen aan PresenterMock-implementatie via interface
PresenterBedrijfslogica, toestandsbeheer, navigatieUnittesten (via ViewContract mock)

Het principe van enkele verantwoordelijkheid wordt in MVP strikter nageleefd dan in MVC: View is alleen verantwoordelijk voor rendering, Model — voor gegevens, Presenter — voor logica en coördinatie. In echte projecten beslaat Presenter 40–60% van de schermcode, View — 20–30%, Model — 20–30%. Deze verdeling maakt het mogelijk om de belangrijkste bedrijfslogica te testen zonder de Android-emulator of iOS-simulator te starten.

MVP in Android: Presenter, ViewContract en Activity

MVP in Android gebruikt Activity of Fragment als View die ViewContract implementeert — een interface met methoden voor gegevensweergave. Presenter wordt aangemaakt in Activity, koppelt View aan zichzelf en beheert het laden van gegevens. Bij schermrotatie wordt Activity opnieuw aangemaakt — Presenter kan worden bewaard via een retain-fragment of externe opslag, wat het probleem van toestandsverlies oplost dat kenmerkend is voor pure MVC.

kotlin
// ViewContract — interface voor communicatie tussen Presenter en View
interface UserView {
    fun showLoading()
    fun hideLoading()
    fun showUser(user: User)
    fun showError(message: String)
}

// Presenter — testbare logica-laag
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 ?: "Onbekende fout")
            }
        }
    }
}

// View (Activity) implementeert de interface
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) { /* UI bijwerken */ }
    override fun showLoading() { /* ProgressBar tonen */ }
    override fun hideLoading() { /* ProgressBar verbergen */ }
    override fun showError(message: String) { /* Snackbar tonen */ }
}

Levenscyclusbeheer — het belangrijkste probleem van MVP op Android. Activity wordt vernietigd bij schermrotatie en presenter.attachView() wordt opnieuw aangeroepen in onCreate(). Als het laden van gegevens asynchroon is (RxJava, coroutines), kan View op het moment van voltooiing zijn losgekoppeld. Oplossing — annuleren van abonnementen in detachView() of gebruik van Loader uit Support Library (voor projecten zonder Jetpack). Bij IT Sectr hebben we jarenlang de combinatie MVP + RxJava gebruikt in commerciële projecten — het patroon is stabiel, maar vereist discipline in abonnementsbeheer.

Retain-fragmenten — mechanisme voor het bewaren van Presenter bij schermrotatie. Fragment zonder UI (setRetainInstance(true)) leeft langer dan Activity en bewaart een verwijzing naar Presenter. Bij het opnieuw aanmaken van Activity geeft het fragment dezelfde Presenter door aan de nieuwe Activity. Retain-fragmenten zijn verouderd sinds AndroidX, maar hun pre-Jetpack equivalent (Fragment.setRetainInstance) werkt nog steeds in legacy-projecten. In moderne ontwikkeling beveelt Google ViewModel aan in plaats van retain-fragmenten.

MVP in iOS: Presenter en View Protocol

MVP in iOS wordt gebouwd via een View-protocol. UIViewController implementeert het protocol, Presenter importeert UIKit niet en wordt zuiver getest. In tegenstelling tot Apple MVC, waar UIViewController zelf logica en directe IBOutlet-verbindingen bevat, beheert Presenter de toestand en geeft opdrachten aan View via protocolmethoden. View neemt geen beslissingen — het voert de opdrachten van Presenter uit: showUser, showLoading, navigateToProfile.

swift
import Foundation

// View Protocol — abstractie voor Presenter
protocol UserViewProtocol: AnyObject {
    func showLoading()
    func hideLoading()
    func display(user: User)
    func displayError(message: String)
}

// Presenter — zuivere logica, zonder 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) implementeert het protocol
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
    }
    // ... overige methoden van het protocol
}

Weak reference naar View — een verplichte voorwaarde in iOS MVP. UIViewController kan worden vernietigd (pop uit navigatiestack) en de closure ervan in Presenter creëert een retain cycle. Een zwakke verwijzing (weak var) garandeert dat View wordt vrijgegeven wanneer het scherm wordt verlaten, ongeacht de aanwezigheid van asynchrone bewerkingen in Presenter. In Android wordt een vergelijkbaar probleem opgelost via detachView() — een aanroep in onDestroy() zet de verwijzing naar View op nul.

Passive View vs Supervising Controller — twee varianten van MVP van Martin Fowler. Passive View: View bevat geen logica, Presenter beheert de toestand volledig. Supervising Controller: View doet zelf eenvoudige gegevensbinding (bijvoorbeeld via data binding), Presenter grijpt alleen in bij complexe scenario's. In mobiele ontwikkeling wordt Passive View vaker gebruikt — het biedt maximale testbaarheid en voorspelbaarheid van de schermtoestand.

Verschillen tussen MVP en MVC en voordelen van testen

Het belangrijkste verschil tussen MVP en MVC — de manier van communicatie met View. In MVC heeft Controller een directe verwijzing naar View (UIViewController.IBOutlets, Activity.findViewById). In MVP communiceert Presenter met View via de ViewContract-interface. Dit verschil verandert de testbaarheid radicaal: een Mock-object dat ViewContract implementeert, maakt het mogelijk de logica van Presenter te controleren zonder de applicatie, emulator en UI-framework te starten.

CriteriumMVCMVP
Verbinding met ViewDirect (Controller → View)Via interface (Presenter → ViewContract)
Logica testenVereist UIKit/Android FrameworkUnittesten zonder platformafhankelijkheden
LevenscyclusController leeft met het schermPresenter kan langer leven (retain)
ComplexiteitMinimaal+1 interface per scherm
Massive ControllerTypisch probleemLogica in Presenter, View dun

Voorbeeld unittest van Presenter in Kotlin: een mock UserView wordt gemaakt, doorgegeven aan Presenter, loadUser wordt aangeroepen, er wordt gecontroleerd of showUser is aangeroepen met correcte gegevens. De test wordt in milliseconden uitgevoerd, geen emulator nodig. Op iOS hetzelfde — OCMock of een protocol-stub controleert aanroepen van UserViewProtocol-methoden. In projecten van IT Sectr met MVP bereikte de dekking van bedrijfslogica met unittesten 85–90%, wat 2–3 keer hoger is dan in vergelijkbare MVC-projecten.

Wanneer MVP de voorkeur heeft boven MVC — in projecten met strenge stabiliteitseisen: bankapplicaties, medische systemen, betaalterminals. In deze domeinen zijn de kosten van een fout hoog en zijn unittesten kritiek. In Post-MVP projecten (wanneer het product al op de markt is, maar de codebasis legacy is) maakt MVP het mogelijk om geleidelijk logica uit Massive View Controller naar een testbare laag te verplaatsen zonder de architectuur volledig te herschrijven.

Beperkingen van MVP en overgang naar MVVM

Belangrijkste nadelen van MVP — toename van het aantal interfaces en handmatig abonnementsbeheer. Elk scherm vereist ten minste één ViewContract + Presenter, voor 50 schermen — 50 interfaces en 50 Presenter-klassen. In MVVM vervangt ViewModel Presenter en gebruikt reactieve mechanismen (LiveData, StateFlow, ObservableObject), wat de noodzaak voor handmatig attach/detach en ViewContract-interfaces elimineert.

RxJava en MVP — populaire combinatie in Android 2015–2019. Presenter abonneert zich op Observable uit Repository, toont resultaat via ViewContract. Probleem: disposable moet expliciet worden geannuleerd in detachView(), anders veroorzaakt een abonnementslek een crash bij het bijwerken van losgekoppelde View. De bibliotheken RxLifecycle en AutoDispose hebben het afmelden gedeeltelijk geautomatiseerd, maar voegden afhankelijkheid toe. Bij IT Sectr zijn we in 2020 overgestapt van MVP+RxJava naar MVVM+Flow — de code werd 25–30% korter dankzij het verdwijnen van ViewContract.

Migratie van MVP naar MVVM — een stapsgewijs proces. 1) Vervang ViewContract door LiveData/StateFlow in Presenter. 2) Verwijder attach/detach methoden — abonnement gaat via observe(). 3) Hernoem Presenter naar ViewModel. 4) Integreer DI (Hilt/Koin) voor ViewModelFactory. Migratie van één scherm duurt 2–4 uur, van de hele codebasis — 2–4 weken voor een project met 50–100 schermen. Na migratie worden ViewContract-interfaces verwijderd, code wordt korter, tests blijven behouden.

MVP in moderne ontwikkeling — het patroon leeft, maar verliest terrein aan MVVM en MVI. Google beveelt officieel MVVM met Jetpack aan voor nieuwe projecten. Apple — MVVM met SwiftUI. Kennis van MVP is echter verplicht voor werk met legacy-code: honderden Android-apps in Google Play werken nog steeds op MVP, waaronder apps van grote banken, retailers en transportbedrijven. Begrip van MVP is de basis voor het beheersen van MVI en Clean Architecture, aangezien Presenter de directe voorouder is van Use Case in de terminologie van Robert Martin.

Veelgestelde vragen

Waarin verschilt MVP van MVC?

In MVP communiceert Presenter met View via de ViewContract-interface, niet direct. In MVC heeft Controller een directe verwijzing naar View via IBOutlet/findViewById. MVP maakt het mogelijk Presenter te testen met unittesten zonder iOS Simulator of Android Emulator, omdat Presenter niet afhankelijk is van UIKit of Android Framework. MVC vereist het starten van de applicatie om de controller te testen.

Wanneer moet ik MVP gebruiken in plaats van MVVM?

MVP is gerechtvaardigd in legacy-projecten die al op dit patroon zijn gebouwd en in applicaties zonder ondersteuning voor reactieve mechanismen (LiveData, StateFlow, Combine). Voor nieuwe projecten beveelt Google MVVM met Jetpack aan (Android) en Apple beveelt MVVM met SwiftUI aan (iOS). MVP blijft de beste keuze voor projecten op pure UIKit zonder Combine wanneer unittesten van bedrijfslogica vereist is.

Hoe los ik het probleem van Presenter-verlies bij schermrotatie op?

Op Android — gebruik een retain-fragment (setRetainInstance(true)) of ViewModel uit Jetpack. Retain-fragment bewaart Presenter bij rotatie en geeft het door aan de nieuwe Activity. ViewModel van Google — een modern alternatief dat automatisch de toestand bewaart bij rotatie zonder retain-fragmenten. Op iOS — Presenter wordt opnieuw aangemaakt bij elke viewDidLoad, maar wordt gecachet in een aparte coördinator-service.

Hoeveel klassen zijn er nodig voor één scherm in MVP?

Minimaal 4: ViewContract-interface, ViewContract-implementatie (Activity/Fragment), Presenter, Model (Repository). Als Dagger/Hilt wordt gebruikt, wordt een DI-module toegevoegd. Voor 50 schermen zijn dat 200+ klassen. MVVM vermindert het aantal met 1 bestand per scherm (ViewContract is niet nodig), MVI voegt State- en Intent-klassen toe. Het aantal klassen — het belangrijkste argument tegen MVP in grote projecten.

Wat is het verschil tussen Passive View en Supervising Controller in MVP?

Passive View — View bevat geen logica, Presenter beheert volledig de toestand en gegevens. Supervising Controller — View doet zelf eenvoudige binding (data binding), Presenter grijpt in bij complexe scenario's. In mobiele ontwikkeling domineert Passive View — het biedt maximale testbaarheid en voorspelbaarheid. Supervising Controller wordt gebruikt in webframeworks (ASP.NET Web Forms, GWT).

Samenvatting

  • MVP (Model-View-Presenter) — evolutie van MVC met een testbare Presenter-laag via de ViewContract-interface
  • ViewContract — interface die View abstraheert van Presenter, waardoor mock-testen mogelijk is
  • Passive View — dominante MVP-variant in mobiele ontwikkeling met passieve View
  • Presenter — bevat bedrijfslogica, is niet afhankelijk van UIKit of Android Framework
  • MVP vs MVC — MVP lost het testprobleem op, maar voegt 1 interface per scherm toe
  • Android retain-fragmenten — bewaren van Presenter bij schermrotatie vóór de komst van Jetpack ViewModel
  • Migratie naar MVVM — vervanging van ViewContract door LiveData/StateFlow verkort code met 25–30%

We ontwikkelen een mobiele applicatie turnkey

IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.

Bespreek het project

Lees ook