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 тестується юніт-тестами, 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Дані, бізнес-логіка, мережеві викликиЮніт-тести (не залежить від UI)
ViewВідображення UI, передача подій PresenterMock-реалізація через інтерфейс
PresenterБізнес-логіка, керування станом, навігаціяЮніт-тести (через ViewContract mock)

Принцип єдиної відповідальності в 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 ?: "Unknown error")
            }
        }
    }
}

// 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 може бути detached. Рішення — скасування підписок в detachView() або використання Loader з Support Library (для проєктів без Jetpack). В IT Sectr ми роками застосовували зв'язку MVP + RxJava в комерційних проєктах — патерн стабільний, але вимагає дисципліни в управлінні підписками.

Ретейн-фрагменти — механізм збереження Presenter при повороті екрану. Фрагмент без UI (setRetainInstance(true)) живе довше за Activity та зберігає посилання на Presenter. При перестворенні Activity фрагмент передає той самий Presenter новій Activity. Ретейн-фрагменти deprecated з AndroidX, але їх pre-Jetpack аналог (Fragment.setRetainInstance) досі працює в легасі-проєктах. У сучасній розробці Google рекомендує ViewModel замість ретейн-фрагментів.

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 з navigation stack), а його замикання в 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 FrameworkЮніт-тести без платформних залежностей
Життєвий циклController живе з екраномPresenter може жити довше (retain)
СкладністьМінімальна+1 інтерфейс на екран
Massive ControllerТипова проблемаЛогіка в Presenter, View тонка

Приклад юніт-тесту Presenter на Kotlin: створюється mock UserView, передається в Presenter, викликається loadUser, перевіряється, що showUser викликаний з коректними даними. Тест виконується за мілісекунди, не потребує емулятора. На iOS аналогічно — OCMock або протокол-стаб перевіряє виклики методів UserViewProtocol. В проєктах IT Sectr з MVP покриття бізнес-логіки юніт-тестами досягало 85–90%, що в 2–3 рази вище, ніж в аналогічних MVC-проєктах.

Коли MVP доцільніший за MVC — в проєктах з жорсткими вимогами до стабільності: банківські застосунки, медичні системи, платіжні термінали. У цих сферах ціна помилки висока, і юніт-тести критичні. В проєктах 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(), інакше витік підписки спричинить краш при оновленні detached 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 юніт-тестами без 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 при вимозі юніт-тестування бізнес-логіки.

Як вирішити проблему втрати 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також