MVP (Model-View-Presenter) — un modèle architectural où le Presenter agit comme intermédiaire entre le Model et la View via l'interface ViewContract. Contrairement à MVC, où le Controller gère directement la View via UIKit, le Presenter ne dépend pas du framework — il fonctionne par abstraction, ce qui le rend testable sans Android SDK ni UIKit. MVP est largement utilisé dans le développement Android avant Jetpack et reste pertinent pour les projets legacy. Plus d'informations dans l'article de Martin Fowler.
Points clés
MVP (Model-View-Presenter) — un modèle architectural proposé par Martin Fowler au début des années 2000 comme évolution de MVC pour améliorer la testabilité de l'interface utilisateur. Le Model gère les données et la logique métier, la View est responsable du rendu et du traitement des entrées utilisateur, le Presenter est le composant central qui reçoit les événements de la View, récupère les données du Model et forme l'état pour l'affichage.
La principale différence entre MVP et MVC — le Presenter n'a pas de référence directe à la View. Au lieu de cela, le Presenter interagit avec la View via l'interface ViewContract. La View implémente cette interface et se transmet au Presenter. Cela rompt la dépendance à UIKit (iOS) ou Android Framework — le Presenter peut être testé isolément avec une implémentation mock de ViewContract. Dans MVC le contrôleur UIViewController met directement à jour UILabel, dans MVP le Presenter appelle view.showName(name), et la View décide comment afficher.
| Composant | Responsabilité | Testabilité |
|---|---|---|
| Model | Données, logique métier, appels réseau | Tests unitaires (indépendant de l'UI) |
| View | Rendu de l'UI, transmission d'événements au Presenter | Implémentation mock via interface |
| Presenter | Logique métier, gestion d'état, navigation | Tests unitaires (via mock de ViewContract) |
Principe de Responsabilité Unique dans MVP est suivi plus strictement que dans MVC : la View est uniquement responsable du rendu, le Model des données, le Presenter de la logique et de la coordination. Dans les projets réels, le Presenter occupe 40–60% du code de l'écran, la View — 20–30%, le Model — 20–30%. Cette répartition permet de tester la logique métier clé sans lancer d'émulateur Android ou de simulateur iOS.
MVP dans Android utilise Activity ou Fragment comme View, qui implémente ViewContract — une interface avec des méthodes d'affichage de données. Le Presenter est créé dans l'Activity, attache la View à lui-même et gère le chargement des données. Lorsque l'écran tourne, l'Activity est recréée — le Presenter peut être préservé via un fragment retain ou un stockage externe, résolvant le problème de perte d'état caractéristique du MVC pur.
// ViewContract — interface pour que le Presenter communique avec la View
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — couche de logique testable
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) implémente l'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) { /* mettre à jour l'UI */ }
override fun showLoading() { /* afficher ProgressBar */ }
override fun hideLoading() { /* masquer ProgressBar */ }
override fun showError(message: String) { /* afficher Snackbar */ }
}
Gestion du cycle de vie — un problème clé de MVP sur Android. L'Activity est détruite lors de la rotation de l'écran, et presenter.attachView() est rappelé dans onCreate(). Si le chargement des données est asynchrone (RxJava, coroutines), au moment de l'achèvement la View peut être detached. La solution — annuler les abonnements dans detachView() ou utiliser le Loader de la Support Library (pour les projets sans Jetpack). Chez IT Sectr nous avons utilisé la combinaison MVP + RxJava pendant des années dans des projets commerciaux — le modèle est stable mais exige de la discipline dans la gestion des abonnements.
Fragments retain — un mécanisme pour préserver le Presenter lors de la rotation de l'écran. Un fragment sans UI (setRetainInstance(true)) survit à l'Activity et conserve une référence au Presenter. Lorsque l'Activity est recréée, le fragment transmet le même Presenter à la nouvelle Activity. Les fragments retain sont dépréciés depuis AndroidX, mais leur analogue pré-Jetpack (Fragment.setRetainInstance) fonctionne encore dans les projets legacy. Dans le développement moderne, Google recommande ViewModel plutôt que les fragments retain.
MVP dans iOS est construit via un protocole View. UIViewController implémente le protocole, le Presenter n'importe pas UIKit et est purement testable. Contrairement à Apple MVC, où UIViewController contient lui-même la logique et des connexions IBOutlet directes, le Presenter gère l'état et commande la View via des méthodes de protocole. La View ne prend pas de décisions — elle exécute les commandes du Presenter : showUser, showLoading, navigateToProfile.
import Foundation
// View Protocol — abstraction pour le Presenter
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — logique pure, sans 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) implémente le protocole
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
}
// ... reste des méthodes du protocole
}
Weak reference sur la View — obligatoire dans MVP iOS. Un UIViewController peut être détruit (pop de la pile de navigation), et sa fermeture dans le Presenter créerait un retain cycle. Une référence faible (weak var) garantit que la View est libérée lorsqu'elle quitte l'écran, indépendamment des opérations asynchrones dans le Presenter. Dans Android un problème similaire est résolu via detachView() — l'appel dans onDestroy() annule la référence à la View.
Passive View vs Supervising Controller — deux variantes de MVP par Martin Fowler. Passive View : la View ne contient pas de logique, le Presenter gère entièrement l'état. Supervising Controller : la View elle-même fait un liaison simple de données (par exemple, via data binding), le Presenter n'intervient que dans les scénarios complexes. Dans le développement mobile, Passive View est utilisé plus souvent — il offre une testabilité maximale et une prédictibilité de l'état de l'écran.
La principale différence entre MVP et MVC est la manière de communiquer avec la View. Dans MVC, le Controller a une référence directe à la View (UIViewController.IBOutlets, Activity.findViewById). Dans MVP, le Presenter interagit avec la View via l'interface ViewContract. Cette différence change fondamentalement la testabilité : un objet mock implémentant ViewContract permet de tester la logique du Presenter sans lancer l'application, l'émulateur ou le framework UI.
| Critère | MVC | MVP |
|---|---|---|
| Connexion avec View | Directe (Controller → View) | Via interface (Presenter → ViewContract) |
| Test de logique | Nécessite UIKit/Android Framework | Tests unitaires sans dépendances plateforme |
| Cycle de vie | Controller vit avec l'écran | Presenter peut survivre (retain) |
| Complexité | Minimale | +1 interface par écran |
| Massive Controller | Problème typique | Logique dans Presenter, View fine |
Exemple de test unitaire du Presenter en Kotlin : un mock UserView est créé, passé au Presenter, loadUser est appelé, on vérifie que showUser a été appelé avec des données correctes. Le test s'exécute en millisecondes, aucun émulateur requis. Sur iOS de même — OCMock ou un stub de protocole vérifie les appels de méthodes UserViewProtocol. Dans les projets IT Sectr avec MVP, la couverture des tests unitaires de la logique métier atteignait 85–90%, soit 2–3 fois plus que dans des projets MVC similaires.
Quand MVP est préférable à MVC — dans les projets avec des exigences strictes de stabilité : applications bancaires, systèmes médicaux, terminaux de paiement. Dans ces domaines, le coût d'une erreur est élevé et les tests unitaires sont critiques. Dans les projets post-MVP (quand le produit est déjà sur le marché mais la base de code est legacy), MVP permet d'extraire progressivement la logique du Massive View Controller vers une couche testable sans réécriture architecturale complète.
Les principaux inconvénients de MVP — la croissance du nombre d'interfaces et la gestion manuelle des abonnements. Chaque écran nécessite au moins un ViewContract + Presenter, pour 50 écrans — 50 interfaces et 50 classes Presenter. Dans MVVM, ViewModel remplace le Presenter et utilise des mécanismes réactifs (LiveData, StateFlow, ObservableObject), éliminant le besoin d'attach/detach manuel et d'interfaces ViewContract.
RxJava et MVP — une combinaison populaire dans Android 2015–2019. Le Presenter s'abonne à un Observable du Repository, affiche le résultat via ViewContract. Le problème : disposable doit être annulé explicitement dans detachView(), sinon une fuite d'abonnement provoquera un crash lors de la mise à jour d'une View detached. Les bibliothèques RxLifecycle et AutoDispose ont partiellement automatisé le désabonnement mais ont ajouté des dépendances. Chez IT Sectr nous sommes passés de MVP+RxJava à MVVM+Flow en 2020 — le code est devenu 25–30% plus court grâce à la suppression de ViewContract.
Migration de MVP vers MVVM — un processus progressif. 1) Remplacer ViewContract par LiveData/StateFlow dans le Presenter. 2) Supprimer les méthodes attach/detach — l'abonnement se fait via observe(). 3) Renommer Presenter en ViewModel. 4) Intégrer DI (Hilt/Koin) pour ViewModelFactory. La migration d'un écran prend 2–4 heures, de toute la base de code — 2–4 semaines pour un projet de 50–100 écrans. Après la migration, les interfaces ViewContract sont supprimées, le code se réduit, les tests restent.
MVP dans le développement moderne — le modèle est vivant mais cède le pas à MVVM et MVI. Google recommande officiellement MVVM avec Jetpack pour les nouveaux projets. Apple — MVVM avec SwiftUI. Cependant, la connaissance de MVP est obligatoire pour travailler avec du code legacy : des centaines d'applications Android sur Google Play fonctionnent encore avec MVP, y compris les applications de grandes banques, détaillants et entreprises de transport. Comprendre MVP est la base pour maîtriser MVI et Clean Architecture, car le Presenter est le prédécesseur direct de Use Case dans les termes de Robert Martin.
Questions fréquentes
Dans MVP, le Presenter interagit avec la View via l'interface ViewContract, pas directement. Dans MVC, le Controller a une référence directe à la View via IBOutlet/findViewById. MVP permet de tester le Presenter avec des tests unitaires sans iOS Simulator ni Android Emulator, car le Presenter ne dépend pas de UIKit ou Android Framework. MVC nécessite le lancement de l'application pour tester le contrôleur.
MVP est justifié dans les projets legacy déjà construits sur ce modèle, et dans les applications sans support de mécanismes réactifs (LiveData, StateFlow, Combine). Pour les nouveaux projets, Google recommande MVVM avec Jetpack (Android) et Apple recommande MVVM avec SwiftUI (iOS). MVP reste le meilleur choix pour les projets sur UIKit pur sans Combine lorsque des tests unitaires de la logique métier sont requis.
Sur Android — utiliser un fragment retain (setRetainInstance(true)) ou ViewModel de Jetpack. Le fragment retain stocke le Presenter lors de la rotation et le transmet à la nouvelle Activity. Le ViewModel de Google est une alternative moderne qui préserve automatiquement l'état lors de la rotation sans fragments retain. Sur iOS — le Presenter est recréé à chaque viewDidLoad mais est mis en cache dans un service coordinateur séparé.
Au moins 4 : interface ViewContract, implémentation de ViewContract (Activity/Fragment), Presenter, Model (Repository). Si Dagger/Hilt est utilisé, un module DI est ajouté. Pour 50 écrans cela fait 200+ classes. MVVM réduit le nombre d'1 fichier par écran (ViewContract n'est pas nécessaire), MVI ajoute des classes State et Intent. Le nombre de classes est le principal argument contre MVP dans les grands projets.
Passive View — la View ne contient pas de logique, le Presenter gère entièrement l'état et les données. Supervising Controller — la View elle-même fait un liaison simple (data binding), le Presenter intervient dans les scénarios complexes. Dans le développement mobile, Passive View domine — il offre une testabilité et une prédictibilité maximales. Supervising Controller est utilisé dans les frameworks web (ASP.NET Web Forms, GWT).
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi