MVP (Model-View-Presenter) — arkitekturmönster där Presenter fungerar som medlare mellan Model och View via ViewContract-gränssnittet. Till skillnad från MVC, där Controller direkt hanterar View via UIKit, är Presenter inte beroende av ramverket — det arbetar genom abstraktion, vilket gör det testbart utan Android SDK eller UIKit. MVP användes i stor utsträckning inom Android-utveckling före Jetpack och förblir relevant för legacy-projekt. Mer i Martin Fowlers artikel.
Huvudpunkter
MVP (Model-View-Presenter) — arkitekturmönster föreslaget av Martin Fowler i början av 2000-talet som en evolution av MVC för att förbättra testbarheten av användargränssnittet. Model hanterar data och affärslogik, View ansvarar för visning och bearbetning av användarinmatning, Presenter — central komponent som tar emot händelser från View, hämtar data från Model och formar tillstånd för visning.
Den huvudsakliga skillnaden mellan MVP och MVC — Presenter har ingen direkt referens till View. Istället interagerar Presenter med View via ViewContract-gränssnittet. View implementerar detta gränssnitt och överför sig själv till Presenter. Detta bryter beroendet av UIKit (iOS) eller Android Framework — Presenter testas isolerat med en mock-implementering av ViewContract. I MVC uppdaterar controller UIViewController direkt UILabel, i MVP anropar Presenter metoden view.showName(name) och View bestämmer hur det ska visas.
| Komponent | Ansvar | Testbarhet |
|---|---|---|
| Model | Data, affärslogik, nätverksanrop | Enhetstester (oberoende av UI) |
| View | Visning av UI, överföring av händelser till Presenter | Mock-implementering via gränssnitt |
| Presenter | Affärslogik, tillståndshantering, navigering | Enhetstester (via ViewContract mock) |
Principen om ensamt ansvar efterlevs striktare i MVP än i MVC: View ansvarar endast för rendering, Model — för data, Presenter — för logik och koordinering. I verkliga projekt upptar Presenter 40–60% av skärmkoden, View — 20–30%, Model — 20–30%. Denna fördelning gör det möjligt att testa viktig affärslogik utan att starta Android-emulatorn eller iOS-simulatorn.
MVP i Android använder Activity eller Fragment som View som implementerar ViewContract — ett gränssnitt med metoder för datavisning. Presenter skapas i Activity, kopplar View till sig själv och hanterar dataladdning. Vid skärmrotation återskapas Activity — Presenter kan bevaras via retain-fragment eller extern lagring, vilket löser problemet med tillståndsförlust som är typiskt för ren MVC.
// ViewContract — gränssnitt för kommunikation mellan Presenter och View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — testbart logiklager
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 ?: "Okänt fel")
}
}
}
}
// View (Activity) implementerar gränssnittet
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) { /* uppdatera UI */ }
override fun showLoading() { /* visa ProgressBar */ }
override fun hideLoading() { /* dölj ProgressBar */ }
override fun showError(message: String) { /* visa Snackbar */ }
}
Livscykelhantering — det viktigaste problemet med MVP på Android. Activity förstörs vid skärmrotation och presenter.attachView() anropas igen i onCreate(). Om dataladdning är asynkron (RxJava, korutiner), kan View vid slutförandet vara frånkopplad. Lösning — avbryta prenumerationer i detachView() eller använda Loader från Support Library (för projekt utan Jetpack). På IT Sectr använde vi i åratal kombinationen MVP + RxJava i kommersiella projekt — mönstret är stabilt men kräver disciplin i prenumerationshantering.
Retain-fragment — mekanism för att bevara Presenter vid skärmrotation. Fragment utan UI (setRetainInstance(true)) lever längre än Activity och lagrar en referens till Presenter. Vid återskapande av Activity överför fragmentet samma Presenter till den nya Activity. Retain-fragment är föråldrade sedan AndroidX, men deras pre-Jetpack-motsvarighet (Fragment.setRetainInstance) fungerar fortfarande i legacy-projekt. I modern utveckling rekommenderar Google ViewModel istället för retain-fragment.
MVP i iOS byggs via ett View-protokoll. UIViewController implementerar protokollet, Presenter importerar inte UIKit och testas rent. Till skillnad från Apple MVC, där UIViewController själv innehåller logik och direkta IBOutlet-anslutningar, hanterar Presenter tillståndet och ger kommandon till View via protokollmetoder. View fattar inga beslut — det utför Presenters kommandon: 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 — ren logik, utan 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) implementerar protokollet
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
}
// ... resten av protokollets metoder
}
Weak reference till View — ett obligatoriskt villkor i iOS MVP. UIViewController kan förstöras (pop från navigeringsstacken) och dess closure i Presenter skapar en retain cycle. En svag referens (weak var) garanterar att View frigörs när den lämnar skärmen, oavsett förekomsten av asynkrona operationer i Presenter. I Android löses ett liknande problem via detachView() — anrop i onDestroy() nollställer referensen till View.
Passive View vs Supervising Controller — två varianter av MVP från Martin Fowler. Passive View: View innehåller ingen logik, Presenter hanterar tillståndet fullständigt. Supervising Controller: View gör själv enkel databindning (t.ex. via data binding), Presenter ingriper endast i komplexa scenarier. Inom mobil utveckling används Passive View oftare — det ger maximal testbarhet och förutsägbarhet av skärmtillståndet.
Den huvudsakliga skillnaden mellan MVP och MVC — sättet att kommunicera med View. I MVC har Controller en direkt referens till View (UIViewController.IBOutlets, Activity.findViewById). I MVP interagerar Presenter med View via ViewContract-gränssnittet. Denna skillnad förändrar testbarheten radikalt: ett Mock-objekt som implementerar ViewContract gör det möjligt att kontrollera Presenters logik utan att starta applikationen, emulatorn och UI-ramverket.
| Kriterium | MVC | MVP |
|---|---|---|
| Anslutning till View | Direkt (Controller → View) | Via gränssnitt (Presenter → ViewContract) |
| Testning av logik | Kräver UIKit/Android Framework | Enhetstester utan plattformsberoenden |
| Livscykel | Controller lever med skärmen | Presenter kan leva längre (retain) |
| Komplexitet | Minimal | +1 gränssnitt per skärm |
| Massive Controller | Typiskt problem | Logik i Presenter, View tunn |
Exempel på enhetstest av Presenter i Kotlin: en mock UserView skapas, skickas till Presenter, loadUser anropas, kontrollera om showUser anropades med korrekt data. Testet körs på millisekunder, kräver ingen emulator. På iOS liknande — OCMock eller protokoll-stubb kontrollerar anrop av UserViewProtocol-metoder. I IT Sectrs projekt med MVP nådde täckningen av affärslogik med enhetstester 85–90%, 2–3 gånger högre än i liknande MVC-projekt.
När MVP är att föredra framför MVC — i projekt med stränga krav på stabilitet: bankapplikationer, medicinska system, betalterminaler. Inom dessa områden är kostnaden för fel hög och enhetstester är kritiska. I Post-MVP-projekt (när produkten redan är på marknaden men kodbasen är legacy) gör MVP det möjligt att gradvis flytta logik från Massive View Controller till en testbar nivå utan att helt skriva om arkitekturen.
De största nackdelarna med MVP — ökat antal gränssnitt och manuell prenumerationshantering. Varje skärm kräver minst ett ViewContract + Presenter, för 50 skärmar — 50 gränssnitt och 50 Presenter-klasser. I MVVM ersätter ViewModel Presenter och använder reaktiva mekanismer (LiveData, StateFlow, ObservableObject), vilket eliminerar behovet av manuell attach/detach och ViewContract-gränssnitt.
RxJava och MVP — populär kombination i Android 2015–2019. Presenter prenumererar på Observable från Repository, visar resultat via ViewContract. Problem: disposable måste explicit avbrytas i detachView(), annars orsakar en prenumerationsläcka en krasch vid uppdatering av frånkopplad View. Biblioteken RxLifecycle och AutoDispose automatiserade delvis avbrutet, men lade till beroende. På IT Sectr bytte vi från MVP+RxJava till MVVM+Flow 2020 — koden blev 25–30% kortare tack vare att ViewContract försvann.
Migrering från MVP till MVVM — en stegvis process. 1) Ersätt ViewContract med LiveData/StateFlow i Presenter. 2) Ta bort metoder för attach/detach — prenumeration sker via observe(). 3) Byt namn på Presenter till ViewModel. 4) Integrera DI (Hilt/Koin) för ViewModelFactory. Migrering av en skärm tar 2–4 timmar, av hela kodbasen — 2–4 veckor för ett projekt med 50–100 skärmar. Efter migrering tas ViewContract-gränssnitten bort, koden förkortas, testerna finns kvar.
MVP i modern utveckling — mönstret lever, men förlorar mot MVVM och MVI. Google rekommenderar officiellt MVVM med Jetpack för nya projekt. Apple — MVVM med SwiftUI. Kunskap om MVP är dock obligatorisk för arbete med legacy-kod: hundratals Android-applikationer i Google Play kör fortfarande på MVP, inklusive applikationer från stora banker, detaljhandlare och transportföretag. Förståelse av MVP är grunden för att bemästra MVI och Clean Architecture, eftersom Presenter är den direkta föregångaren till Use Case i Robert Martins terminologi.
Vanliga frågor
I MVP interagerar Presenter med View via ViewContract-gränssnittet, inte direkt. I MVC har Controller en direkt referens till View via IBOutlet/findViewById. MVP gör det möjligt att testa Presenter med enhetstester utan iOS Simulator eller Android Emulator, eftersom Presenter inte är beroende av UIKit eller Android Framework. MVC kräver att applikationen startas för att testa controllern.
MVP är motiverat i legacy-projekt som redan är byggda på detta mönster och i applikationer utan stöd för reaktiva mekanismer (LiveData, StateFlow, Combine). För nya projekt rekommenderar Google MVVM med Jetpack (Android) och Apple rekommenderar MVVM med SwiftUI (iOS). MVP förblir det bästa valet för projekt på ren UIKit utan Combine när enhetstestning av affärslogik krävs.
På Android — använd retain-fragment (setRetainInstance(true)) eller ViewModel från Jetpack. Retain-fragment bevarar Presenter vid rotation och överför det till den nya Activity. ViewModel från Google — ett modernt alternativ som automatiskt bevarar tillståndet vid rotation utan retain-fragment. På iOS — Presenter skapas på nytt vid varje viewDidLoad, men cachas i en separat koordinatortjänst.
Minimum 4: ViewContract-gränssnitt, ViewContract-implementering (Activity/Fragment), Presenter, Model (Repository). Om Dagger/Hilt används, läggs en DI-modul till. För 50 skärmar är det 200+ klasser. MVVM minskar antalet med 1 fil per skärm (ViewContract behövs inte), MVI lägger till State- och Intent-klasser. Antalet klasser — det främsta argumentet mot MVP i stora projekt.
Passive View — View innehåller ingen logik, Presenter hanterar fullständigt tillstånd och data. Supervising Controller — View gör själv enkel bindning (data binding), Presenter ingriper i komplexa scenarier. Inom mobil utveckling dominerar Passive View — ger maximal testbarhet och förutsägbarhet. Supervising Controller används i webbramverk (ASP.NET Web Forms, GWT).
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också