Architecture en Couches
Architecture en couches pour monorepo Flutter — quatre couches organisées en tant que packages Dart indépendants avec des dépendances strictement unidirectionnelles.
Standards de Base
Appliquez ces standards à TOUS les travaux d'architecture en couches :
- Quatre couches — Data, Repository, Business Logic, Presentation — chaque feature couvre exactement ces quatre couches
- Dépendances unidirectionnelles — Presentation → Business Logic → Repository → Data — ne jamais ignorer ou inverser une couche
- Les couches Data et Repository vivent dans
packages/— chacune est un package Dart indépendant avec son proprepubspec.yaml - Business Logic et Presentation vivent dans
lib/— organisées par feature dans l'app - Les packages de la couche Data ne contiennent zéro logique domaine/métier — ils doivent être réutilisables dans des projets non liés
- Pas de dépendances inter-repositories — les repositories ne s'importent jamais les uns les autres
- Pas de Flutter SDK dans les packages data ou repository — scaffoldez avec l'outil MCP
very_good_clicreate dart_package - Un repository par domaine —
user_repository,weather_repository,auth_repository - Dépendances de chemin pour les packages locaux — jamais
git:ou références de version pub pour les packages du même repo - Barrel exports à chaque limite de package —
src/n'est jamais importé directement par les consommateurs - Les repositories acceptent les dépendances de la couche data via injection au constructeur — ne jamais instancier les clients en interne
- App bootstrap câble toutes les couches —
main_<flavor>.dartcrée les clients et repositories, les fournit viaRepositoryProvider
Aperçu de l'Architecture
| Couche | Responsabilité | Localisation | Dépend de | Exemple |
|---|---|---|---|---|
| Data | Communication externe — appels API, stockage local, plugins plateforme | packages/<name>_api_client/ |
Packages externes uniquement | user_api_client, local_storage_client |
| Repository | Orchestration de données — combine sources, transforme modèles, met en cache | packages/<name>_repository/ |
Packages couche Data | user_repository, weather_repository |
| Business Logic | Gestion d'état — traite actions utilisateur, émet changements d'état | lib/<feature>/bloc/ ou lib/<feature>/cubit/ |
Couche Repository | LoginBloc, ProfileCubit |
| Presentation | UI — widgets, pages, vues, layout | lib/<feature>/view/ |
Couche Business Logic | LoginPage, ProfileView |
┌─────────────────────────────────────────────┐
│ Presentation │
│ (lib/<feature>/view/) │
└──────────────────┬──────────────────────────┘
│ lit l'état / envoie événements
┌──────────────────▼──────────────────────────┐
│ Business Logic │
│ (lib/<feature>/bloc/) │
└──────────────────┬──────────────────────────┘
│ appelle méthodes repository
┌──────────────────▼──────────────────────────┐
│ Repository │
│ (packages/<name>_repository/) │
└──────────────────┬──────────────────────────┘
│ appelle clients data
┌──────────────────▼──────────────────────────┐
│ Data │
│ (packages/<name>_api_client/) │
└─────────────────────────────────────────────┘
Structure du Monorepo
my_app/
├── lib/
│ ├── app/
│ │ ├── app.dart # Fichier barrel
│ │ └── view/
│ │ └── app.dart # Widget App avec MultiRepositoryProvider
│ ├── login/ # Feature : login
│ │ ├── login.dart # Fichier barrel
│ │ ├── bloc/
│ │ │ ├── login_bloc.dart
│ │ │ ├── login_event.dart
│ │ │ └── login_state.dart
│ │ └── view/
│ │ ├── login_page.dart # Page fournit Bloc
│ │ └── login_view.dart # View consomme l'état
│ ├── profile/ # Feature : profile
│ │ ├── profile.dart
│ │ ├── cubit/
│ │ │ ├── profile_cubit.dart
│ │ │ └── profile_state.dart
│ │ └── view/
│ │ ├── profile_page.dart
│ │ └── profile_view.dart
│ ├── main_development.dart # Entrypoint flavor
│ ├── main_staging.dart
│ └── main_production.dart
├── packages/
│ ├── auth_api_client/ # Couche Data : auth API
│ │ ├── lib/
│ │ │ ├── auth_api_client.dart # Fichier barrel
│ │ │ └── src/
│ │ │ ├── auth_api_client.dart
│ │ │ └── models/
│ │ │ ├── models.dart
│ │ │ └── auth_response.dart
│ │ └── pubspec.yaml
│ ├── local_storage_client/ # Couche Data : stockage local
│ │ ├── lib/
│ │ │ ├── local_storage_client.dart
│ │ │ └── src/
│ │ │ └── local_storage_client.dart
│ │ └── pubspec.yaml
│ ├── auth_repository/ # Couche Repository : auth
│ │ ├── lib/
│ │ │ ├── auth_repository.dart # Fichier barrel
│ │ │ └── src/
│ │ │ ├── auth_repository.dart
│ │ │ └── models/
│ │ │ ├── models.dart
│ │ │ └── user.dart # Modèle domaine
│ │ └── pubspec.yaml
│ └── user_repository/ # Couche Repository : user
│ ├── lib/
│ │ ├── user_repository.dart
│ │ └── src/
│ │ ├── user_repository.dart
│ │ └── models/
│ │ ├── models.dart
│ │ └── user_profile.dart
│ └── pubspec.yaml
├── test/
│ └── ... # Reflète la structure de lib/
└── pubspec.yaml # Root app pubspec
Couche Data
La couche data gère toute communication externe. Chaque package data enveloppe une unique source externe (REST API, base de données locale, plugin plateforme) et expose des méthodes typées et des modèles de réponse.
Règles :
- Les modèles représentent la forme externe des données — correspondent exactement au schéma API/stockage
- Pas d'imports Flutter — utilisez l'outil MCP
very_good_clicreate dart_package - Injection au constructeur des clients HTTP pour la testabilité
- Les modèles de réponse utilisent les factories
fromJson/toJson - Exportez tout via un fichier barrel — n'exposez jamais
src/
Pattern : Classe Data Client
Injectez le client HTTP au constructeur pour la testabilité. Retournez des modèles de réponse typés — jamais du JSON brut.
/// Client HTTP pour l'API User.
class UserApiClient {
// http.Client injecté — les tests passent un mock, la production obtient un vrai client
UserApiClient({
required String baseUrl,
http.Client? httpClient,
}) : _baseUrl = baseUrl,
_httpClient = httpClient ?? http.Client();
final String _baseUrl;
final http.Client _httpClient;
/// Chaque méthode retourne un modèle de réponse typé.
Future<UserResponse> getUser(String userId) async {
final response = await _httpClient.get(
Uri.parse('$_baseUrl/users/$userId'),
);
if (response.statusCode != 200) {
throw UserApiException(response.statusCode, response.body);
}
return UserResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>,
);
}
}
Consultez worked-example.md pour le package complet user_api_client avec pubspec, fichiers barrel, modèles de réponse et classe exception.
Couche Repository
La couche repository orchestre les sources de données et expose les modèles domaine. Chaque repository compose un ou plusieurs clients data, transforme les modèles de réponse en modèles domaine, et fournit une API propre pour la couche business logic.
Règles :
- Pas de dépendances inter-repositories — les repositories sont isolés
- Pas de Flutter SDK — l'outil MCP
very_good_clicreate dart_package - Les modèles domaine vivent dans le package repository — pas dans les packages data
- Transformez les modèles data en modèles domaine — ne laissez jamais fuir les formes de réponse API en amont
- Acceptez tous les clients data via injection au constructeur
Pattern : Modèle Domaine + Transformation Repository
Les modèles domaine étendent Equatable et représentent la forme interne des données de l'app — distincte de la forme de réponse API. La méthode repository transforme entre les deux.
/// Modèle domaine — vit dans le package repository, PAS le package data.
/// Les champs correspondent aux besoins de l'app, pas au schéma API.
class User extends Equatable {
const User({
required this.id,
required this.email,
required this.displayName,
this.avatarUrl,
});
final String id;
final String email;
final String displayName;
final String? avatarUrl;
@override
List<Object?> get props => [id, email, displayName, avatarUrl];
}
/// Repository accepte client data via constructeur — ne crée jamais le sien.
class UserRepository {
const UserRepository({
required UserApiClient userApiClient,
}) : _userApiClient = userApiClient;
final UserApiClient _userApiClient;
/// Transforme UserResponse (forme API) → User (forme domaine).
Future<User> getUser(String userId) async {
final response = await _userApiClient.getUser(userId);
return User(
id: response.id,
email: response.email,
displayName: response.displayName,
avatarUrl: response.avatarUrl,
);
}
}
Consultez worked-example.md pour le package complet user_repository avec pubspec, fichiers barrel et gestion d'erreurs. Consultez model-transformation.md pour les patterns de transformation détaillés entre modèles data et domaine.
Graphe de Dépendances
Le pubspec.yaml de chaque couche applique l'architecture via des dépendances de chemin.
Package Data (packages/user_api_client/pubspec.yaml)
dependencies:
# Packages externes uniquement — pas de dépendances locales
http: ^1.4.0
json_annotation: ^4.9.0
Package Repository (packages/user_repository/pubspec.yaml)
dependencies:
equatable: ^2.0.7
# Dépendance de chemin sur le package couche data
user_api_client:
path: ../user_api_client
Root App (pubspec.yaml)
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^9.1.0
# Packages repository uniquement — les packages data sont transitifs
auth_repository:
path: packages/auth_repository
user_repository:
path: packages/user_repository
L'app ne dépend jamais directement des packages data. Les packages data sont des dépendances transitives via les repositories. Cela applique la limite de couche — la business logic et presentation ne peuvent pas contourner la couche repository.
Flux de Données
Walkthrough étape par étape : l'utilisateur tape le bouton "Load Profile".
- Presentation envoie un événement —
context.read<ProfileBloc>().add(ProfileLoadRequested(userId: '123')) - Business Logic appelle le repository — le handler Bloc invoque
_userRepository.getUser(event.userId)et émet l'état selon le résultat - Repository appelle le client data —
UserRepository.getUserdélègue à_userApiClient.getUseret transforme la réponse en unUserdomaine - Couche Data communique avec la source externe —
UserApiClient.getUsereffectue la requête HTTP et retourne unUserResponsetypé - Les données remontent — Presentation se reconstruit via
BlocBuilderselon le nouvel état
// lib/profile/bloc/profile_bloc.dart
Future<void> _onLoadRequested(
ProfileLoadRequested event,
Emitter<ProfileState> emit,
) async {
emit(const ProfileState.loading());
try {
final user = await _userRepository.getUser(event.userId);
emit(ProfileState.success(user: user));
} on UserNotFoundException {
emit(const ProfileState.notFound());
} catch (_) {
emit(const ProfileState.failure());
}
}
Consultez data-flow.md pour le walkthrough complet du flux de données avec le code à chaque couche.
App Bootstrap
Le main_<flavor>.dart de l'app crée tous les clients data et repositories, puis les passe au widget App. MultiRepositoryProvider rend les repositories disponibles à tout l'arbre de widgets.
lib/main_development.dart
import 'package:flutter/material.dart';
import 'package:my_app/app/app.dart';
import 'package:auth_api_client/auth_api_client.dart';
import 'package:local_storage_client/local_storage_client.dart';
import 'package:auth_repository/auth_repository.dart';
import 'package:user_api_client/user_api_client.dart';
import 'package:user_repository/user_repository.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
const baseUrl = 'https://api.dev.example.com';
// Couche Data
final authApiClient = AuthApiClient(baseUrl: baseUrl);
final userApiClient = UserApiClient(baseUrl: baseUrl);
final localStorageClient = LocalStorageClient();
// Couche Repository
final authRepository = AuthRepository(
authApiClient: authApiClient,
localStorageClient: localStorageClient,
);
final userRepository = UserRepository(
userApiClient: userApiClient,
);
runApp(
App(
authRepository: authRepository,
userRepository: userRepository,
),
);
}
Les flavors changent seulement la configuration (URLs de base, clés API) — l'architecture reste identique sur development, staging et production. Consultez worked-example.md pour le widget App avec MultiRepositoryProvider.
Anti-Patterns
| Anti-Pattern | Problème | Approche Correcte |
|---|---|---|
| Widget appelle directement le client API | Contourne les couches Repository et Business Logic — pas de transformation, pas de gestion d'état | Widget envoie événement → Bloc appelle Repository → Repository appelle client API |
| Repository importe un autre repository | Crée des graphes de dépendances circulaires ou enchevêtrées — casse la testabilité indépendante | Chaque repository est autonome ; combinez les données au niveau Bloc si nécessaire |
| Modèles domaine dans la couche data | Couple la forme externe API à la forme domaine interne — les changements API cassent toute l'app | La couche Data a les modèles de réponse ; la couche Repository a les modèles domaine avec transformation |
| Business logic dans le repository | Le repository devient un monolithe intestable mélangeant orchestration et règles | Le repository transforme les données ; Bloc/Cubit contient toutes les règles métier |
git: ou version pub pour packages locaux |
Casse le monorepo — les changements nécessitent des cycles publish/push au lieu d'éditions locales instantanées | Utilisez des dépendances path: pour tous les packages du monorepo |
| Imports Flutter dans packages data/repository | Empêche les packages d'être utilisés dans des contextes Dart-only (outils CLI, serveurs) | Scaffoldez avec l'outil MCP very_good_cli create dart_package — pas de dépendance Flutter SDK |
| Un giant repository pour tout | God-object avec trop de responsabilités — impossible à tester en isolation | Un repository par limite domaine (user_repository, settings_repository) |
Importer src/ directement |
Casse l'encapsulation — les consommateurs dépendent de la structure interne | Exportez l'API publique via fichiers barrel ; importez le package, jamais de chemins src/ |
Workflows Courants
Ajouter une Nouvelle Source de Données
- Scaffoldez le package avec l'outil MCP
very_good_clicreate dart_package:<name>_api_client --output-directory packages - Ajoutez les dépendances externes à
pubspec.yaml(p. ex.,http,json_annotation) - Créez les modèles de réponse dans
lib/src/models/avecfromJson/toJson - Créez le fichier barrel
lib/src/models/models.dartexportant tous les modèles - Implémentez la classe client dans
lib/src/<name>_api_client.dart - Créez le fichier barrel du package
lib/<name>_api_client.dartexportant les contenus desrc/ - Écrivez les tests unitaires dans
test/reflétant la structure delib/— consultez la skill testing - Utilisez l'outil MCP
very_good_clitestsur le répertoire du package — passezdirectory: 'packages/<name>_api_client'pour limiter l'exécution
Ajouter un Nouveau Repository
- Scaffoldez le package avec l'outil MCP
very_good_clicreate dart_package:<name>_repository --output-directory packages - Ajoutez les dépendances de chemin sur les packages de la couche data dans
pubspec.yaml - Ajoutez
equatableaux dépendances pour les modèles domaine - Créez les modèles domaine dans
lib/src/models/étendantEquatable - Créez le fichier barrel
lib/src/models/models.dart - Implémentez la classe repository avec les clients data injectés au constructeur
- Ajoutez la logique de transformation des modèles de réponse en modèles domaine
- Créez le fichier barrel du package
lib/<name>_repository.dart - Écrivez les tests unitaires avec les clients data mockés — consultez la skill testing
Connecter un Repository à une Feature
- Ajoutez une dépendance de chemin sur le package repository au
pubspec.yamlroot - Créez le repository dans
main_<flavor>.dartet passez-le àApp - Ajoutez
RepositoryProvider.valuedans leMultiRepositoryProviderd'App - Créez le Bloc/Cubit avec le repository injecté — consultez la skill bloc
- Construisez la Page/View avec
BlocProvideretBlocBuilder— consultez la skill bloc
Ressources Additionnelles
- Exemple complet travaillé et référence pubspec
- Patterns de transformation de modèles — conversion modèle data vs modèle domaine
- Tests au niveau du package — test des clients data et repositories en isolation
- Pour les patterns Bloc/Cubit et la séparation Page/View — consultez la skill bloc
- Pour le scaffolding de projet, utilisez l'outil MCP
very_good_clicreate dart_package - Pour les tests des clients data, repositories et Blocs — consultez la skill testing