vgv-layered-architecture

Par verygoodopensource · vgv-ai-flutter-plugin

Bonnes pratiques pour l'architecture monorepo en couches VGV dans Flutter.

npx skills add https://github.com/verygoodopensource/vgv-ai-flutter-plugin --skill vgv-layered-architecture

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 propre pubspec.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_cli create dart_package
  • Un repository par domaineuser_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 packagesrc/ 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 couchesmain_<flavor>.dart crée les clients et repositories, les fournit via RepositoryProvider

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_cli create 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_cli create 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".

  1. Presentation envoie un événement — context.read<ProfileBloc>().add(ProfileLoadRequested(userId: '123'))
  2. Business Logic appelle le repository — le handler Bloc invoque _userRepository.getUser(event.userId) et émet l'état selon le résultat
  3. Repository appelle le client data — UserRepository.getUser délègue à _userApiClient.getUser et transforme la réponse en un User domaine
  4. Couche Data communique avec la source externe — UserApiClient.getUser effectue la requête HTTP et retourne un UserResponse typé
  5. Les données remontent — Presentation se reconstruit via BlocBuilder selon 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

  1. Scaffoldez le package avec l'outil MCP very_good_cli create dart_package : <name>_api_client --output-directory packages
  2. Ajoutez les dépendances externes à pubspec.yaml (p. ex., http, json_annotation)
  3. Créez les modèles de réponse dans lib/src/models/ avec fromJson/toJson
  4. Créez le fichier barrel lib/src/models/models.dart exportant tous les modèles
  5. Implémentez la classe client dans lib/src/<name>_api_client.dart
  6. Créez le fichier barrel du package lib/<name>_api_client.dart exportant les contenus de src/
  7. Écrivez les tests unitaires dans test/ reflétant la structure de lib/ — consultez la skill testing
  8. Utilisez l'outil MCP very_good_cli test sur le répertoire du package — passez directory: 'packages/<name>_api_client' pour limiter l'exécution

Ajouter un Nouveau Repository

  1. Scaffoldez le package avec l'outil MCP very_good_cli create dart_package : <name>_repository --output-directory packages
  2. Ajoutez les dépendances de chemin sur les packages de la couche data dans pubspec.yaml
  3. Ajoutez equatable aux dépendances pour les modèles domaine
  4. Créez les modèles domaine dans lib/src/models/ étendant Equatable
  5. Créez le fichier barrel lib/src/models/models.dart
  6. Implémentez la classe repository avec les clients data injectés au constructeur
  7. Ajoutez la logique de transformation des modèles de réponse en modèles domaine
  8. Créez le fichier barrel du package lib/<name>_repository.dart
  9. Écrivez les tests unitaires avec les clients data mockés — consultez la skill testing

Connecter un Repository à une Feature

  1. Ajoutez une dépendance de chemin sur le package repository au pubspec.yaml root
  2. Créez le repository dans main_<flavor>.dart et passez-le à App
  3. Ajoutez RepositoryProvider.value dans le MultiRepositoryProvider d'App
  4. Créez le Bloc/Cubit avec le repository injecté — consultez la skill bloc
  5. Construisez la Page/View avec BlocProvider et BlocBuilder — consultez la skill bloc

Ressources Additionnelles

Skills similaires