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 тестируется 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-тесты (через 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. Retain-фрагменты deprecated с AndroidX, но их pre-Jetpack аналог (Fragment.setRetainInstance) до сих пор работает в легаси-проектах. В современной разработке 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 из 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 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(), иначе утечка подписки вызовет краш при обновлении 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 unit-тестами без 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 при требовании 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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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