MVP (Model-View-Presenter) — ein Architekturmuster, bei dem der Presenter als Vermittler zwischen dem Model und der View über die ViewContract-Schnittstelle agiert. Im Gegensatz zu MVC, wo der Controller die View direkt über UIKit steuert, ist der Presenter nicht vom Framework abhängig — er arbeitet über Abstraktion, was ihn ohne Android SDK oder UIKit testbar macht. MVP wurde in der Android-Entwicklung vor Jetpack häufig eingesetzt und bleibt für Legacy-Projekte relevant. Mehr Details im Artikel von Martin Fowler.
Das Wichtigste
MVP (Model-View-Presenter) — ein Architekturmuster, das Martin Fowler Anfang der 2000er Jahre als Weiterentwicklung von MVC zur Verbesserung der Testbarkeit der Benutzeroberfläche vorschlug. Das Model verwaltet Daten und Geschäftslogik, die View ist für die Darstellung und Verarbeitung von Benutzereingaben zuständig, der Presenter ist die zentrale Komponente, die Ereignisse von der View empfängt, Daten aus dem Model abruft und den Zustand für die Anzeige bildet.
Der Hauptunterschied zwischen MVP und MVC — der Presenter hat keinen direkten Verweis auf die View. Stattdessen interagiert der Presenter über die ViewContract-Schnittstelle mit der View. Die View implementiert diese Schnittstelle und übergibt sich selbst an den Presenter. Dadurch wird die Abhängigkeit von UIKit (iOS) oder Android Framework aufgehoben — der Presenter kann isoliert mit einer Mock-Implementierung von ViewContract getestet werden. In MVC aktualisiert der UIViewController-Controller direkt UILabel, in MVP ruft der Presenter view.showName(name) auf, und die View entscheidet, wie angezeigt wird.
| Komponente | Verantwortung | Testbarkeit |
|---|---|---|
| Model | Daten, Geschäftslogik, Netzwerkaufrufe | Unit-Tests (unabhängig von UI) |
| View | UI-Darstellung, Weiterleitung von Ereignissen an Presenter | Mock-Implementierung über Schnittstelle |
| Presenter | Geschäftslogik, Zustandsverwaltung, Navigation | Unit-Tests (über ViewContract-Mock) |
Das Prinzip der einzigen Verantwortung wird in MVP strenger befolgt als in MVC: Die View ist nur für die Darstellung zuständig, das Model für Daten, der Presenter für Logik und Koordination. In realen Projekten nimmt der Presenter 40–60% des Bildschirmcodes ein, die View — 20–30%, das Model — 20–30%. Diese Verteilung ermöglicht das Testen der wichtigsten Geschäftslogik ohne Starten eines Android-Emulators oder iOS-Simulators.
MVP in Android verwendet Activity oder Fragment als View, die ViewContract implementiert — eine Schnittstelle mit Methoden zur Datenanzeige. Der Presenter wird in der Activity erstellt, bindet die View an sich und verwaltet das Laden von Daten. Wenn der Bildschirm gedreht wird, wird die Activity neu erstellt — der Presenter kann über ein Retain-Fragment oder externen Speicher erhalten bleiben, was das für reines MVC charakterische Problem des Zustandsverlusts löst.
// ViewContract — Schnittstelle zur Verbindung von Presenter mit View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — testbare Logikschicht
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 ?: "Unknown error")
}
}
}
}
// View (Activity) implementiert die Schnittstelle
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 aktualisieren */ }
override fun showLoading() { /* ProgressBar anzeigen */ }
override fun hideLoading() { /* ProgressBar ausblenden */ }
override fun showError(message: String) { /* Snackbar anzeigen */ }
}
Lebenszyklusverwaltung — ein Hauptproblem von MVP auf Android. Die Activity wird bei Bildschirmdrehung zerstört, und presenter.attachView() wird in onCreate() erneut aufgerufen. Wenn das Laden von Daten asynchron ist (RxJava, Coroutinen), kann die View zum Zeitpunkt des Abschlusses bereits getrennt sein. Lösung — Abonnementkündigung in detachView() oder Verwendung des Loader aus der Support Library (für Projekte ohne Jetpack). Bei IT Sectr haben wir jahrelang die Kombination MVP + RxJava in kommerziellen Projekten eingesetzt — das Muster ist stabil, erfordert aber Disziplin bei der Abonnementverwaltung.
Retain-Fragmente — ein Mechanismus zur Erhaltung des Presenters bei Bildschirmdrehung. Ein Fragment ohne UI (setRetainInstance(true)) überlebt die Activity und behält einen Verweis auf den Presenter. Bei der Neuerstellung der Activity übergibt das Fragment denselben Presenter an die neue Activity. Retain-Fragmente sind seit AndroidX veraltet, aber ihr Pre-Jetpack-Analogon (Fragment.setRetainInstance) funktioniert noch in Legacy-Projekten. In der modernen Entwicklung empfiehlt Google ViewModel anstelle von Retain-Fragmenten.
MVP in iOS wird über ein View-Protokoll aufgebaut. UIViewController implementiert das Protokoll, der Presenter importiert UIKit nicht und ist rein testbar. Im Gegensatz zu Apple MVC, wo UIViewController selbst Logik und direkte IBOutlet-Verbindungen enthält, verwaltet der Presenter den Zustand und steuert die View über Protokollmethoden. Die View trifft keine Entscheidungen — sie führt Presenter-Befehle aus: showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — Abstraktion für Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — reine Logik, ohne 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) implementiert das Protokoll
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
}
// ... restliche Protokollmethoden
}
Weak Reference auf die View — eine zwingende Voraussetzung in iOS MVP. Ein UIViewController kann zerstört werden (Pop aus dem Navigation Stack), und sein Closure im Presenter würde einen Retain Cycle erzeugen. Eine schwache Referenz (weak var) stellt sicher, dass die View freigegeben wird, wenn sie den Bildschirm verlässt, unabhängig von asynchronen Operationen im Presenter. In Android wird ein ähnliches Problem über detachView() gelöst — der Aufruf in onDestroy() setzt den Verweis auf die View zurück.
Passive View vs Supervising Controller — zwei MVP-Varianten von Martin Fowler. Passive View: Die View enthält keine Logik, der Presenter verwaltet den Zustand vollständig. Supervising Controller: Die View selbst führt einfaches Datenbinding durch (z. B. über Data Binding), der Presenter greift nur bei komplexen Szenarien ein. In der mobilen Entwicklung wird Passive View häufiger verwendet — er bietet maximale Testbarkeit und Vorhersagbarkeit des Bildschirmzustands.
Der Hauptunterschied zwischen MVP und MVC ist die Art der Kommunikation mit der View. In MVC hat der Controller einen direkten Verweis auf die View (UIViewController.IBOutlets, Activity.findViewById). In MVP interagiert der Presenter über die ViewContract-Schnittstelle mit der View. Dieser Unterschied verändert die Testbarkeit grundlegend: Ein Mock-Objekt, das ViewContract implementiert, ermöglicht das Testen der Presenter-Logik ohne Starten der App, des Emulators oder des UI-Frameworks.
| Kriterium | MVC | MVP |
|---|---|---|
| View-Verbindung | Direkt (Controller → View) | Über Schnittstelle (Presenter → ViewContract) |
| Logiktest | Erfordert UIKit/Android Framework | Unit-Tests ohne Plattformabhängigkeiten |
| Lebenszyklus | Controller lebt mit dem Bildschirm | Presenter kann überleben (retain) |
| Komplexität | Minimal | +1 Schnittstelle pro Bildschirm |
| Massive Controller | Typisches Problem | Logik im Presenter, View dünn |
Beispiel eines Presenter-Unit-Tests in Kotlin: Ein Mock-UserView wird erstellt, an den Presenter übergeben, loadUser wird aufgerufen, es wird geprüft, ob showUser mit korrekten Daten aufgerufen wurde. Der Test wird in Millisekunden ausgeführt, kein Emulator erforderlich. Auf iOS ähnlich — OCMock oder ein Protokoll-Stub überprüft UserViewProtocol-Methodenaufrufe. In IT Sectr-Projekten mit MVP erreichte die Unit-Test-Abdeckung der Geschäftslogik 85–90%, was 2–3 Mal höher ist als in ähnlichen MVC-Projekten.
Wann MVP MVC vorzuziehen ist — in Projekten mit strengen Stabilitätsanforderungen: Bank-Apps, medizinische Systeme, Zahlungsterminals. In diesen Bereichen sind die Kosten eines Fehlers hoch, und Unit-Tests sind kritisch. In Post-MVP-Projekten (wenn das Produkt bereits auf dem Markt ist, aber die Codebasis Legacy ist) ermöglicht MVP das schrittweise Auslagern der Logik aus dem Massive View Controller in eine testbare Schicht ohne vollständige architektonische Neuschreibung.
Die Hauptnachteile von MVP — die Zunahme der Anzahl von Schnittstellen und die manuelle Abonnementverwaltung. Jeder Bildschirm erfordert mindestens ein ViewContract + Presenter, für 50 Bildschirme — 50 Schnittstellen und 50 Presenter-Klassen. In MVVM ersetzt ViewModel den Presenter und verwendet reaktive Mechanismen (LiveData, StateFlow, ObservableObject), wodurch die Notwendigkeit von manuellem Attach/Detach und ViewContract-Schnittstellen entfällt.
RxJava und MVP — eine beliebte Kombination in Android 2015–2019. Der Presenter abonniert ein Observable aus dem Repository, zeigt das Ergebnis über ViewContract an. Problem: Disposable muss in detachView() explizit gekündigt werden, sonst verursacht ein Abonnement-Leck einen Absturz beim Aktualisieren einer getrennten View. Die Bibliotheken RxLifecycle und AutoDispose automatisierten die Kündigung teilweise, fügten aber Abhängigkeiten hinzu. Bei IT Sectr sind wir 2020 von MVP+RxJava auf MVVM+Flow umgestiegen — der Code wurde durch Wegfall von ViewContract um 25–30% kürzer.
Migration von MVP zu MVVM — ein schrittweiser Prozess. 1) ViewContract im Presenter durch LiveData/StateFlow ersetzen. 2) Attach/Detach-Methoden entfernen — das Abonnement erfolgt über observe(). 3) Presenter in ViewModel umbenennen. 4) DI (Hilt/Koin) für ViewModelFactory integrieren. Die Migration eines Bildschirms dauert 2–4 Stunden, der gesamten Codebasis — 2–4 Wochen für ein Projekt mit 50–100 Bildschirmen. Nach der Migration werden ViewContract-Schnittstellen entfernt, der Code schrumpft, die Tests bleiben.
MVP in der modernen Entwicklung — das Muster lebt, weicht aber MVVM und MVI. Google empfiehlt offiziell MVVM mit Jetpack für neue Projekte. Apple — MVVM mit SwiftUI. Jedoch sind MVP-Kenntnisse für die Arbeit mit Legacy-Code unerlässlich: Hunderte Android-Apps im Google Play laufen noch mit MVP, darunter Apps großer Banken, Einzelhändler und Transportunternehmen. Das Verständnis von MVP ist die Grundlage für die Beherrschung von MVI und Clean Architecture, da der Presenter der direkte Vorgänger von Use Case in Robert Martins Begriffen ist.
Häufig gestellte Fragen
In MVP interagiert der Presenter über die ViewContract-Schnittstelle mit der View, nicht direkt. In MVC hat der Controller über IBOutlet/findViewById einen direkten Verweis auf die View. MVP ermöglicht das Testen des Presenters mit Unit-Tests ohne iOS Simulator oder Android Emulator, da der Presenter nicht von UIKit oder Android Framework abhängt. MVC erfordert das Starten der App zum Testen des Controllers.
MVP ist in Legacy-Projekten gerechtfertigt, die bereits auf diesem Muster aufbauen, und in Apps ohne Unterstützung reaktiver Mechanismen (LiveData, StateFlow, Combine). Für neue Projekte empfiehlt Google MVVM mit Jetpack (Android) und Apple MVVM mit SwiftUI (iOS). MVP bleibt die beste Wahl für Projekte mit reinem UIKit ohne Combine, wenn Unit-Tests der Geschäftslogik erforderlich sind.
Auf Android — ein Retain-Fragment (setRetainInstance(true)) oder ViewModel von Jetpack verwenden. Das Retain-Fragment speichert den Presenter bei Drehung und übergibt ihn an die neue Activity. Das ViewModel von Google ist eine moderne Alternative, die den Zustand bei Drehung automatisch ohne Retain-Fragmente erhält. Auf iOS — der Presenter wird bei jedem viewDidLoad neu erstellt, aber in einem separaten Koordinator-Dienst zwischengespeichert.
Mindestens 4: ViewContract-Schnittstelle, ViewContract-Implementierung (Activity/Fragment), Presenter, Model (Repository). Bei Verwendung von Dagger/Hilt wird ein DI-Modul hinzugefügt. Für 50 Bildschirme sind das 200+ Klassen. MVVM reduziert die Anzahl um 1 Datei pro Bildschirm (ViewContract nicht erforderlich), MVI fügt State- und Intent-Klassen hinzu. Die Anzahl der Klassen ist das Hauptargument gegen MVP in großen Projekten.
Passive View — die View enthält keine Logik, der Presenter verwaltet Zustand und Daten vollständig. Supervising Controller — die View selbst führt einfaches Binding (Data Binding) durch, der Presenter greift nur bei komplexen Szenarien ein. In der mobilen Entwicklung dominiert Passive View — er bietet maximale Testbarkeit und Vorhersagbarkeit. Supervising Controller wird in Web-Frameworks (ASP.NET Web Forms, GWT) verwendet.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch