MVP (Model-View-Presenter) — архитектурен модел, в който Presenter действа като посредник между Model и View чрез интерфейса ViewContract. За разлика от MVC, където Controller директно управлява View чрез UIKit, Presenter не зависи от рамката — работи чрез абстракция, което го прави тестван без Android SDK или UIKit. MVP се използва широко в Android разработката преди появата на Jetpack и остава актуален за legacy проекти. Повече в статията на Мартин Фаулър.
Основни точки
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-фрагментите са остарели от AndroidX, но техният pre-Jetpack еквивалент (Fragment.setRetainInstance) все още работи в legacy проекти. В съвременното разработване 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 е задължително за работа с legacy код: стотици 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 е оправдан в legacy проекти, вече изградени върху този модел, и в приложения без поддръжка на реактивни механизми (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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също