MVP (Model-View-Presenter) — архитектурални образац у којем Presenter делује као посредник између Model и View преко интерфејса ViewContract. За разлику од MVC, где Controller директно управља View преко UIKit, Presenter не зависи од фрејмворка — ради кроз апстракцију, што га чини тестибилним без Android SDK или UIKit. MVP се широко примењује у Android развоју пре појаве Jetpack и остаје актуелан за легаси пројекте. Више у чланку Мартина Фаулера.
Главно
MVP (Model-View-Presenter) — архитектурални образац који је предложио Мартин Фаулер почетком 2000-их као еволуција MVC за побољшање тестибилности корисничког интерфејса. Model управља подацима и бизнис логиком, View је одговоран за приказ и обраду корисничког уноса, Presenter — централна компонента која прима догађаје од View, преузима податке из Model и формира стање за приказ.
Главна разлика између MVP и MVC — Presenter нема директну референцу на View. Уместо тога, Presenter комуницира са View преко интерфејса ViewContract. View имплементира овај интерфејс и преноси себе Presenter-у. Ово прекида зависност од UIKit (iOS) или Android Framework — Presenter се тестира изоловано са mock имплементацијом ViewContract. У MVC контролер UIViewController директно ажурира UILabel, у MVP Presenter позива метод view.showName(name), а View одлучује како да прикаже.
| Компонента | Одговорност | Тестибилност |
|---|---|---|
| Model | Подаци, бизнис логика, мрежни позиви | Unit-тестови (не зависи од UI) |
| View | Приказ UI, пренос догађаја Presenter-у | Mock имплементација кроз интерфејс |
| Presenter | Бизнис логика, управљање стањем, навигација | Unit-тестови (преко mock ViewContract) |
Принцип јединствене одговорности у MVP се поштује строже него у MVC: View је одговоран само за рендеровање, Model — за податке, Presenter — за логику и координацију. У реалним пројектима Presenter заузима 40–60% кода екрана, View — 20–30%, Model — 20–30%. Оваква расподела омогућава тестирање кључне бизнис логике без покретања Android емулатора или iOS симулатора.
MVP у Android користи Activity или Fragment као View која имплементира ViewContract — интерфејс са методама за приказ података. Presenter се креира у Activity, прикачи View на себе и управља учитавањем података. При ротацији екрана Activity се поново креира — Presenter се може сачувати кроз retain-фрагмент или екстерно складиштење, што решава проблем губитка стања карактеристичан за чисти MVC.
// ViewContract — интерфејс за повезивање Presenter са View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — тестибилни слој логике
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 ?: "Непозната грешка")
}
}
}
}
// View (Activity) имплементира интерфејс
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 */ }
override fun showLoading() { /* показать ProgressBar */ }
override fun hideLoading() { /* скрыть ProgressBar */ }
override fun showError(message: String) { /* показать Snackbar */ }
}
Управљање животним циклусом — кључни проблем MVP на Android-у. Activity се уништава при ротацији екрана, а presenter.attachView() се поново позива у onCreate(). Ако је учитавање података асинхроно (RxJava, корутине), у тренутку завршетка View може бити одвојена. Решење — отказивање претплата у detachView() или коришћење Loader из Support Library (за пројекте без Jetpack). У IT Sectr смо годинама примењивали комбинацију MVP + RxJava у комерцијалним пројектима — образац је стабилан, али захтева дисциплину у управљању претплатама.
Retain-фрагменти — механизам очувања Presenter при ротацији екрана. Фрагмент без UI (setRetainInstance(true)) живи дуже од Activity и чува референцу на Presenter. При поновном креирању Activity фрагмент преноси исти Presenter новој Activity. Retain-фрагменти су deprecated од AndroidX, али њихов pre-Jetpack аналог (Fragment.setRetainInstance) и даље ради у легаси пројектима. У савременом развоју Google препоручује ViewModel уместо retain-фрагмената.
MVP у iOS се гради кроз протокол View. UIViewController имплементира протокол, Presenter не увози UIKit и чисто се тестира. За разлику од Apple MVC, где UIViewController сам садржи логику и директне IBOutlet везе, Presenter управља стањем и командује View кроз методе протокола. View не доноси одлуке — извршава команде Presenter-а: showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — апстракција за Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — чиста логика, без 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) имплементира протокол
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
}
// ... остале методе протокола
}
Weak reference на View — обавезан услов у iOS MVP. UIViewController може бити уништен (pop из навигационог стека), а његов closure у Presenter-у ће створити retain cycle. Слаба референца (weak var) гарантује да се View ослобађа када напусти екран, без обзира на присуство асинхроних операција у Presenter-у. У Android-у сличан проблем се решава кроз detachView() — позив у onDestroy() поништава референцу на View.
Passive View vs Supervising Controller — две варијанте MVP од Мартина Фаулера. Passive View: View не садржи логику, Presenter у потпуности управља стањем. Supervising Controller: View сама ради једноставно везивање података (нпр. кроз data binding), Presenter интервенише само у сложеним сценаријима. У мобилном развоју Passive View се чешће примењује — даје максималну тестибилност и предвидљивост стања екрана.
Главна разлика између MVP и MVC — начин повезивања са View. У MVC Controller има директну референцу на View (UIViewController.IBOutlets, Activity.findViewById). У MVP Presenter комуницира са View преко интерфејса ViewContract. Ова разлика коренито мења тестибилност: Mock објекат који имплементира ViewContract омогућава проверу логике Presenter-а без покретања апликације, емулатора и UI фрејмворка.
| Критеријум | MVC | MVP |
|---|---|---|
| Веза са View | Директна (Controller → View) | Преко интерфејса (Presenter → ViewContract) |
| Тестирање логике | Захтева UIKit/Android Framework | Unit-тестови без платформских зависности |
| Животни циклус | Controller живи са екраном | Presenter може живети дуже (retain) |
| Сложеност | Минимална | +1 интерфејс по екрану |
| Massive Controller | Типичан проблем | Логика у Presenter-у, View танка |
Пример unit-теста Presenter у Kotlin: креира се mock UserView, прослеђује Presenter-у, позива loadUser, проверава да ли је showUser позван са исправним подацима. Тест се извршава у милисекундама, не захтева емулатор. На iOS-у слично — OCMock или стаб протокола проверава позиве метода UserViewProtocol. У пројектима IT Sectr са MVP покривеност бизнис логике unit-тестовима достизала је 85–90%, што је 2–3 пута више него у сличним MVC пројектима.
Када је MVP пожељнији од MVC — у пројектима са строгим захтевима за стабилношћу: банкарске апликације, медицински системи, платни терминали. У овим областима цена грешке је висока, а unit-тестови су критични. У Post-MVP пројектима (када је производ већ на тржишту, али база кода legacy) MVP омогућава постепено извлачење логике из Massive View Controller у тестибилни слој без потпуног преписивања архитектуре.
Основни недостаци MVP — пораст броја интерфејса и ручно управљање претплатама. Сваки екран захтева најмање један ViewContract + Presenter, за 50 екрана — 50 интерфејса и 50 класа Presenter. У MVVM ViewModel замењује Presenter и користи реактивне механизме (LiveData, StateFlow, ObservableObject), што елиминише потребу за ручним attach/detach и интерфејсима ViewContract.
RxJava и MVP — популарна комбинација у Android 2015–2019. Presenter се претплаћује на Observable из Repository, приказује резултат кроз ViewContract. Проблем: disposable мора експлицитно да се откаже у detachView(), иначе цурење претплате ће изазвати crash при ажурирању одвојене View. Библиотеке RxLifecycle и AutoDispose су делимично аутоматизовале отказивање, али су додале зависност. У IT Sectr смо прешли са MVP+RxJava на MVVM+Flow 2020. године — код је постао краћи за 25–30% захваљујући уклањању ViewContract.
Миграција са MVP на MVVM — процес у фазама. 1) Заменити ViewContract са LiveData/StateFlow у Presenter-у. 2) Уклонити методе attach/detach — претплата иде кроз observe(). 3) Преименовати Presenter у ViewModel. 4) Интегрисати DI (Hilt/Koin) за ViewModelFactory. Миграција једног екрана траје 2–4 сата, целе базе кода — 2–4 недеље за пројекат од 50–100 екрана. Након миграције ViewContract интерфејси се уклањају, код се скраћује, тестови остају.
MVP у савременом развоју — образац живи, али попушта пред MVVM и MVI. Google званично препоручује MVVM са Jetpack за нове пројекте. Apple — MVVM са SwiftUI. Међутим, познавање MVP је обавезно за рад са легаси кодом: стотине Android апликација у Google Play и даље раде на MVP, укључујући апликације великих банака, трговаца и транспортних компанија. Разумевање MVP је темељ за савладавање MVI и Clean Architecture, јер је Presenter директни претходник Use Case у терминологији Роберта Мартина.
Често постављана питања
У MVP Presenter комуницира са View преко интерфејса ViewContract, а не директно. У MVC Controller има директну референцу на View преко IBOutlet/findViewById. MVP омогућава тестирање Presenter unit-тестовима без iOS Simulator или Android Emulator, јер Presenter не зависи од UIKit или Android Framework. MVC захтева покретање апликације за тестирање контролера.
MVP је оправдан у легаси пројектима већ изграђеним на овом обрасцу и у апликацијама без подршке за реактивне механизме (LiveData, StateFlow, Combine). За нове пројекте Google препоручује MVVM са Jetpack (Android), а Apple препоручује MVVM са SwiftUI (iOS). MVP остаје најбољи избор за пројекте на чистом UIKit без Combine када је потребно unit-тестирање бизнис логике.
На Android-у — користити retain-фрагмент (setRetainInstance(true)) или ViewModel из Jetpack-а. Retain-фрагмент чува Presenter при ротацији и преноси га новој Activity. ViewModel од Google-а — модерна алтернатива која аутоматски чува стање при ротацији без retain-фрагмената. На iOS-у — Presenter се креира изнова при сваком viewDidLoad, али се кешира у посебном сервису координатору.
Минимум 4: интерфејс ViewContract, имплементација ViewContract (Activity/Fragment), Presenter, Model (Repository). Ако се користе Dagger/Hilt, додаје се DI модул. За 50 екрана то је 200+ класа. MVVM смањује број за 1 фајл по екрану (ViewContract није потребан), MVI додаје State и Intent класе. Број класа — главни аргумент против MVP у великим пројектима.
Passive View — View не садржи логику, Presenter у потпуности управља стањем и подацима. Supervising Controller — View сама ради једноставно везивање (data binding), Presenter интервенише у сложеним сценаријима. У мобилном развоју доминира Passive View — даје максималну тестибилност и предвидљивост. Supervising Controller се примењује у веб фрејмворцима (ASP.NET Web Forms, GWT).
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође