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 | Данные, бизнес-логика, сетевые вызовы | Unit-тесты (не зависит от UI) |
| View | Отображение UI, передача событий Presenter | Mock-реализация через интерфейс |
| Presenter | Бизнес-логика, управление состоянием, навигация | Unit-тесты (через 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. Retain-фрагменты deprecated с AndroidX, но их pre-Jetpack аналог (Fragment.setRetainInstance) до сих пор работает в легаси-проектах. В современной разработке 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 из 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 | 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(), иначе утечка подписки вызовет краш при обновлении 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 unit-тестами без iOS Simulator или Android Emulator, так как Presenter не зависит от UIKit или Android Framework. MVC требует запуска приложения для тестирования контроллера.
MVP оправдан в легаси-проектах, уже построенных на этом паттерне, и в приложениях без поддержки реактивных механизмов (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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также