MVP — шта је то, образац Model-View-Presenter у iOS и Android

Аутор: IT Sectr Објављено: 2026-02-16 Време читања: 10 мин

MVP (Model-View-Presenter) — архитектурални образац у којем Presenter делује као посредник између Model и View преко интерфејса ViewContract. За разлику од MVC, где Controller директно управља View преко UIKit, Presenter не зависи од фрејмворка — ради кроз апстракцију, што га чини тестибилним без Android SDK или UIKit. MVP се широко примењује у Android развоју пре појаве Jetpack и остаје актуелан за легаси пројекте. Више у чланку Мартина Фаулера.

Главно

  • MVP — три компоненте: Model (подаци), View (интерфејс), Presenter (логика и стање)
  • ViewContract — интерфејс кроз који Presenter комуницира са View, обезбеђујући тестибилност
  • Presenter — садржи сву бизнис логику, не зависи од платформских класа Android/iOS
  • Passive View — View је максимално пасивна, само приказује податке по командама Presenter
  • MVP vs MVC — Presenter се тестира unit-тестовима, Controller у MVC зависи од UIKit/Android Framework

Шта је MVP: суштина обрасца Model-View-Presenter

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: Presenter, ViewContract и Activity

MVP у Android користи Activity или Fragment као View која имплементира ViewContract — интерфејс са методама за приказ података. Presenter се креира у Activity, прикачи View на себе и управља учитавањем података. При ротацији екрана Activity се поново креира — Presenter се може сачувати кроз retain-фрагмент или екстерно складиштење, што решава проблем губитка стања карактеристичан за чисти MVC.

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

MVP у iOS се гради кроз протокол View. UIViewController имплементира протокол, Presenter не увози UIKit и чисто се тестира. За разлику од Apple MVC, где UIViewController сам садржи логику и директне IBOutlet везе, Presenter управља стањем и командује View кроз методе протокола. View не доноси одлуке — извршава команде Presenter-а: showUser, showLoading, navigateToProfile.

swift
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 и предности тестирања

Главна разлика између MVP и MVC — начин повезивања са View. У MVC Controller има директну референцу на View (UIViewController.IBOutlets, Activity.findViewById). У MVP Presenter комуницира са View преко интерфејса ViewContract. Ова разлика коренито мења тестибилност: Mock објекат који имплементира ViewContract омогућава проверу логике Presenter-а без покретања апликације, емулатора и UI фрејмворка.

КритеријумMVCMVP
Веза са ViewДиректна (Controller → View)Преко интерфејса (Presenter → ViewContract)
Тестирање логикеЗахтева UIKit/Android FrameworkUnit-тестови без платформских зависности
Животни циклус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 и прелазак на MVVM

Основни недостаци 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 разликује од MVC?

У MVP Presenter комуницира са View преко интерфејса ViewContract, а не директно. У MVC Controller има директну референцу на View преко IBOutlet/findViewById. MVP омогућава тестирање Presenter unit-тестовима без iOS Simulator или Android Emulator, јер Presenter не зависи од UIKit или Android Framework. MVC захтева покретање апликације за тестирање контролера.

Када користити MVP уместо MVVM?

MVP је оправдан у легаси пројектима већ изграђеним на овом обрасцу и у апликацијама без подршке за реактивне механизме (LiveData, StateFlow, Combine). За нове пројекте Google препоручује MVVM са Jetpack (Android), а Apple препоручује MVVM са SwiftUI (iOS). MVP остаје најбољи избор за пројекте на чистом UIKit без Combine када је потребно unit-тестирање бизнис логике.

Како решити проблем губитка Presenter при ротацији екрана?

На Android-у — користити retain-фрагмент (setRetainInstance(true)) или ViewModel из Jetpack-а. Retain-фрагмент чува Presenter при ротацији и преноси га новој Activity. ViewModel од Google-а — модерна алтернатива која аутоматски чува стање при ротацији без retain-фрагмената. На iOS-у — Presenter се креира изнова при сваком viewDidLoad, али се кешира у посебном сервису координатору.

Колико класа је потребно за један екран у MVP?

Минимум 4: интерфејс ViewContract, имплементација ViewContract (Activity/Fragment), Presenter, Model (Repository). Ако се користе Dagger/Hilt, додаје се DI модул. За 50 екрана то је 200+ класа. MVVM смањује број за 1 фајл по екрану (ViewContract није потребан), MVI додаје State и Intent класе. Број класа — главни аргумент против MVP у великим пројектима.

Која је разлика између Passive View и Supervising Controller у MVP?

Passive View — View не садржи логику, Presenter у потпуности управља стањем и подацима. Supervising Controller — View сама ради једноставно везивање (data binding), Presenter интервенише у сложеним сценаријима. У мобилном развоју доминира Passive View — даје максималну тестибилност и предвидљивост. Supervising Controller се примењује у веб фрејмворцима (ASP.NET Web Forms, GWT).

Закључци

  • MVP (Model-View-Presenter) — еволуција MVC са тестибилним слојем Presenter кроз интерфејс ViewContract
  • ViewContract — интерфејс који апстрахује View од Presenter-а, омогућавајући mock тестирање
  • Passive View — доминантна варијанта MVP у мобилном развоју са пасивним View
  • Presenter — садржи бизнис логику, не зависи од UIKit или Android Framework
  • MVP vs MVC — MVP решава проблем тестирања, али додаје 1 интерфејс по екрану
  • Android retain-фрагменти — очување Presenter при ротацији екрана пре појаве Jetpack ViewModel
  • Миграција на MVVM — замена ViewContract са LiveData/StateFlow скраћује код за 25–30%

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође