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 и остава актуален за legacy проекти. Повече в статията на Мартин Фаулър.

Основни точки

  • 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, предаване на събития на PresenterMock имплементация чрез интерфейс
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-фрагментите са остарели от AndroidX, но техният pre-Jetpack еквивалент (Fragment.setRetainInstance) все още работи в legacy проекти. В съвременното разработване 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 е задължително за работа с legacy код: стотици 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 е оправдан в legacy проекти, вече изградени върху този модел, и в приложения без поддръжка на реактивни механизми (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 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също