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 | Дані, бізнес-логіка, мережеві виклики | Юніт-тести (не залежить від UI) |
| View | Відображення UI, передача подій Presenter | Mock-реалізація через інтерфейс |
| Presenter | Бізнес-логіка, керування станом, навігація | Юніт-тести (через ViewContract mock) |
Принцип єдиної відповідальності в 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 ?: "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 будується через протокол 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 з 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 — спосіб зв'язку з 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 | Юніт-тести без платформних залежностей |
| Життєвий цикл | 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 — зростання кількості інтерфейсів та ручне керування підписками. Кожен екран потребує мінімум один 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 Presenter взаємодіє з View через інтерфейс ViewContract, а не безпосередньо. В MVC Controller має пряме посилання на View через IBOutlet/findViewById. MVP дозволяє тестувати Presenter юніт-тестами без iOS Simulator або Android Emulator, оскільки Presenter не залежить від UIKit або Android Framework. MVC потребує запуску застосунку для тестування контролера.
MVP виправданий в легасі-проєктах, вже побудованих на цьому патерні, та в застосунках без підтримки реактивних механізмів (LiveData, StateFlow, Combine). Для нових проєктів Google рекомендує MVVM з Jetpack (Android) та Apple рекомендує MVVM зі SwiftUI (iOS). MVP залишається найкращим вибором для проєктів на чистому UIKit без Combine при вимозі юніт-тестування бізнес-логіки.
На 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також