MVP (Model-View-Presenter) — um padrão arquitetural onde o Presenter atua como intermediário entre o Model e a View através da interface ViewContract. Diferente do MVC, onde o Controller gerencia diretamente a View através do UIKit, o Presenter não depende do framework — ele trabalha através de abstração, o que o torna testável sem Android SDK ou UIKit. O MVP é amplamente usado no desenvolvimento Android antes do Jetpack e continua relevante para projetos legados. Mais informações no artigo de Martin Fowler.
Principais pontos
MVP (Model-View-Presenter) — um padrão arquitetural proposto por Martin Fowler no início dos anos 2000 como evolução do MVC para melhorar a testabilidade da interface do usuário. O Model gerencia dados e lógica de negócio, a View é responsável pela renderização e processamento de entrada do usuário, o Presenter é o componente central que recebe eventos da View, recupera dados do Model e forma o estado para exibição.
A principal diferença entre MVP e MVC — o Presenter não tem uma referência direta à View. Em vez disso, o Presenter interage com a View através da interface ViewContract. A View implementa esta interface e passa-se ao Presenter. Isso quebra a dependência do UIKit (iOS) ou Android Framework — o Presenter pode ser testado isoladamente com uma implementação mock do ViewContract. No MVC o controlador UIViewController atualiza diretamente o UILabel, no MVP o Presenter chama view.showName(name), e a View decide como exibir.
| Componente | Responsabilidade | Testabilidade |
|---|---|---|
| Model | Dados, lógica de negócio, chamadas de rede | Testes unitários (independente de UI) |
| View | Renderização de UI, passagem de eventos ao Presenter | Implementação mock via interface |
| Presenter | Lógica de negócio, gerenciamento de estado, navegação | Testes unitários (via mock do ViewContract) |
Princípio da Responsabilidade Única no MVP é seguido mais estritamente que no MVC: a View é responsável apenas pela renderização, o Model pelos dados, o Presenter pela lógica e coordenação. Em projetos reais o Presenter ocupa 40–60% do código da tela, a View — 20–30%, o Model — 20–30%. Esta distribuição permite testar a lógica de negócio principal sem iniciar um emulador Android ou simulador iOS.
MVP no Android usa Activity ou Fragment como View, que implementa ViewContract — uma interface com métodos de exibição de dados. O Presenter é criado na Activity, anexa a View a si mesmo e gerencia o carregamento de dados. Quando a tela gira, a Activity é recriada — o Presenter pode ser preservado através de um retain fragment ou armazenamento externo, resolvendo o problema de perda de estado característico do MVC puro.
// ViewContract — interface para o Presenter se comunicar com a View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — camada de lógica testável
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) implementa a interface
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) { /* atualizar UI */ }
override fun showLoading() { /* mostrar ProgressBar */ }
override fun hideLoading() { /* ocultar ProgressBar */ }
override fun showError(message: String) { /* mostrar Snackbar */ }
}
Gerenciamento de ciclo de vida — um problema chave do MVP no Android. A Activity é destruída ao girar a tela, e presenter.attachView() é chamado novamente em onCreate(). Se o carregamento de dados for assíncrono (RxJava, corrotinas), no momento da conclusão a View pode estar detached. A solução — cancelar as assinaturas em detachView() ou usar o Loader da Support Library (para projetos sem Jetpack). Na IT Sectr usamos a combinação MVP + RxJava por anos em projetos comerciais — o padrão é estável mas requer disciplina no gerenciamento de assinaturas.
Retain fragments — um mecanismo para preservar o Presenter ao girar a tela. Um fragment sem UI (setRetainInstance(true)) sobrevive à Activity e mantém uma referência ao Presenter. Quando a Activity é recriada, o fragment passa o mesmo Presenter para a nova Activity. Retain fragments estão obsoletos desde AndroidX, mas seu análogo pré-Jetpack (Fragment.setRetainInstance) ainda funciona em projetos legados. No desenvolvimento moderno o Google recomenda ViewModel em vez de retain fragments.
MVP no iOS é construído através de um protocolo View. UIViewController implementa o protocolo, o Presenter não importa UIKit e é puramente testável. Diferente do Apple MVC, onde UIViewController contém lógica e conexões IBOutlet diretas, o Presenter gerencia o estado e comanda a View através de métodos do protocolo. A View não toma decisões — ela executa comandos do Presenter: showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — abstração para o Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — lógica pura, sem 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) implementa o protocolo
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
}
// ... restante dos métodos do protocolo
}
Weak reference à View — obrigatória no iOS MVP. Um UIViewController pode ser destruído (pop da navigation stack), e seu closure no Presente criaria um retain cycle. Uma referência fraca (weak var) garante que a View seja liberada quando sai da tela, independentemente de operações assíncronas no Presenter. No Android um problema similar é resolvido através de detachView() — chamá-lo em onDestroy() anula a referência à View.
Passive View vs Supervising Controller — duas variantes do MVP por Martin Fowler. Passive View: a View não contém lógica, o Presenter gerencia completamente o estado. Supervising Controller: a View mesma faz binding simples de dados (por exemplo, através de data binding), o Presenter só intervém em cenários complexos. No desenvolvimento mobile o Passive View é usado com mais frequência — ele proporciona máxima testabilidade e previsibilidade do estado da tela.
A principal diferença entre MVP e MVC é a forma de comunicação com a View. No MVC o Controller tem uma referência direta à View (UIViewController.IBOutlets, Activity.findViewById). No MVP o Presenter interage com a View através da interface ViewContract. Esta diferença muda fundamentalmente a testabilidade: um objeto mock implementando ViewContract permite testar a lógica do Presenter sem iniciar o aplicativo, emulador ou framework de UI.
| Critério | MVC | MVP |
|---|---|---|
| Conexão com View | Direta (Controller → View) | Através de interface (Presenter → ViewContract) |
| Teste de lógica | Requer UIKit/Android Framework | Testes unitários sem dependências de plataforma |
| Ciclo de vida | Controller vive com a tela | Presenter pode sobreviver (retain) |
| Complexidade | Mínima | +1 interface por tela |
| Massive Controller | Problema típico | Lógica no Presenter, View fina |
Exemplo de teste unitário do Presenter em Kotlin: um mock UserView é criado, passado ao Presenter, loadUser é chamado, verifica-se que showUser foi chamado com dados corretos. O teste executa em milissegundos, não requer emulador. No iOS similarmente — OCMock ou um stub de protocolo verifica chamadas de métodos UserViewProtocol. Em projetos da IT Sectr com MVP, a cobertura de testes unitários da lógica de negócio alcançava 85–90%, 2–3 vezes maior que em projetos MVC similares.
Quando MVP é preferível ao MVC — em projetos com requisitos rigorosos de estabilidade: aplicativos bancários, sistemas médicos, terminais de pagamento. Nestes domínios o custo de um erro é alto, e os testes unitários são críticos. Em projetos pós-MVP (quando o produto já está no mercado mas a base de código é legada), o MVP permite extrair gradualmente a lógica do Massive View Controller para uma camada testável sem uma reescrita arquitetônica completa.
As principais desvantagens do MVP — o crescimento do número de interfaces e o gerenciamento manual de assinaturas. Cada tela requer pelo menos um ViewContract + Presenter, para 50 telas — 50 interfaces e 50 classes Presenter. No MVVM, o ViewModel substitui o Presenter e usa mecanismos reativos (LiveData, StateFlow, ObservableObject), eliminando a necessidade de attach/detach manual e interfaces ViewContract.
RxJava e MVP — uma combinação popular no Android 2015–2019. O Presenter assina um Observable do Repository, exibe o resultado através do ViewContract. O problema: disposable deve ser cancelado explicitamente em detachView(), caso contrário um vazamento de assinatura causará uma falha ao atualizar uma View detached. As bibliotecas RxLifecycle e AutoDispose automatizaram parcialmente o cancelamento mas adicionaram dependências. Na IT Sectr migramos de MVP+RxJava para MVVM+Flow em 2020 — o código ficou 25–30% mais curto devido à eliminação do ViewContract.
Migração de MVP para MVVM — um processo gradual. 1) Substituir ViewContract por LiveData/StateFlow no Presenter. 2) Remover métodos attach/detach — a assinatura vai através de observe(). 3) Renomear Presenter para ViewModel. 4) Integrar DI (Hilt/Koin) para ViewModelFactory. Migrar uma tela leva 2–4 horas, toda a base de código — 2–4 semanas para um projeto de 50–100 telas. Após a migração, as interfaces ViewContract são removidas, o código diminui, os testes permanecem.
MVP no desenvolvimento moderno — o padrão está vivo mas cede para MVVM e MVI. O Google recomenda oficialmente MVVM com Jetpack para novos projetos. A Apple — MVVM com SwiftUI. No entanto, o conhecimento de MVP é obrigatório para trabalhar com código legado: centenas de aplicativos Android no Google Play ainda funcionam com MVP, incluindo aplicativos de grandes bancos, varejistas e empresas de transporte. Entender MVP é a base para dominar MVI e Clean Architecture, já que o Presenter é o predecessor direto de Use Case nos termos de Robert Martin.
Perguntas Frequentes
No MVP, o Presenter interage com a View através da interface ViewContract, não diretamente. No MVC, o Controller tem uma referência direta à View através de IBOutlet/findViewById. O MVP permite testar o Presenter com testes unitários sem iOS Simulator ou Android Emulator, já que o Presenter não depende de UIKit ou Android Framework. O MVC requer iniciar o aplicativo para testar o controlador.
O MVP é justificado em projetos legados já construídos sobre este padrão, e em aplicativos sem suporte a mecanismos reativos (LiveData, StateFlow, Combine). Para novos projetos o Google recomenda MVVM com Jetpack (Android) e a Apple recomenda MVVM com SwiftUI (iOS). O MVP continua sendo a melhor escolha para projetos em UIKit puro sem Combine quando testes unitários da lógica de negócio são necessários.
No Android — usar um retain fragment (setRetainInstance(true)) ou ViewModel do Jetpack. O retain fragment armazena o Presenter ao girar e o passa para a nova Activity. O ViewModel do Google é uma alternativa moderna que preserva o estado automaticamente ao girar sem retain fragments. No iOS — o Presenter é recriado em cada viewDidLoad mas é armazenado em cache em um serviço coordenador separado.
Pelo menos 4: interface ViewContract, implementação do ViewContract (Activity/Fragment), Presenter, Model (Repository). Se Dagger/Hilt for usado, um módulo DI é adicionado. Para 50 telas são 200+ classes. O MVVM reduz a quantidade em 1 arquivo por tela (ViewContract não é necessário), o MVI adiciona classes State e Intent. O número de classes é o principal argumento contra o MVP em projetos grandes.
Passive View — a View não contém lógica, o Presenter gerencia completamente o estado e os dados. Supervising Controller — a View mesma faz binding simples (data binding), o Presenter intervém em cenários complexos. No desenvolvimento mobile o Passive View domina — proporciona máxima testabilidade e previsibilidade. O Supervising Controller é usado em frameworks web (ASP.NET Web Forms, GWT).
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também