--- name: layered-architecture description: > VGV layered monorepo architecture in Flutter: four layers Data, Repository, Business Logic, and Presentation, unidirectional dependency rules, and model transformation across layers. Use when structuring a multi-package Flutter app, creating data or repository packages, defining layer boundaries, or wiring packages in app bootstrap via path dependencies, barrel exports, and RepositoryProvider. Use too when asked to put a domain model or shared class in an api_client or data package, or to add a cross-package dependency the layers forbid. Also use for a new app described in one line ("I'm starting a weather app that reads from a REST API") plus a request for the package, project, folder, or directory layout, or for where a file or feature should live and which package code belongs in. Applies even when the user never says monorepo, layer, or architecture. allowed-tools: Read Glob Grep mcp__very-good-cli__create mcp__very-good-cli__packages_get mcp__very-good-cli__test effort: high --- # Layered Architecture Layered monorepo architecture for Flutter apps — four layers organized as independent Dart packages with strict unidirectional dependencies. --- ## Core Standards Apply these standards to all layered architecture work: - **Four layers** — Data, Repository, Business Logic, Presentation — a feature spans all four whenever its repository reads an external source - **Unidirectional dependencies** — Presentation → Business Logic → Repository → Data — never skip or invert a layer - **Data and Repository layers live in `packages/`** — each is an independent Dart package with its own `pubspec.yaml` - **Business Logic and Presentation live in `lib/`** — organized by feature within the app - **Data layer packages contain zero domain/business logic** — they must be reusable in unrelated projects - **No inter-repository dependencies** — repositories never import other repositories - **No Flutter SDK in data or repository packages** — scaffold with the `very_good_cli` MCP server `create dart_package` tool - **One repository per domain** — `user_repository`, `weather_repository`, `auth_repository` - **Path dependencies for local packages** — never `git:` or pub version references for packages in the same repo - **Barrel exports at every package boundary** — `src/` is never imported directly by consumers - **Repositories take every external source through the constructor** — a data client or an SDK object such as `FirebaseAuth.instance`, built in bootstrap and passed in, never constructed or defaulted inside the repository - **App bootstrap wires all layers** — `main_.dart` creates clients and repositories, provides them via `RepositoryProvider` - **Only the app's entrypoints and bootstrap import a data package** — blocs, widgets, and app tests reach data through a repository - **Dart 3.13 primary constructors** — on a Dart 3.13+ baseline, declare model and widget fields as primary-constructor declaring parameters (`class const User(final String id, final String name) extends Equatable`) rather than `this.field`; keep the classic form only below 3.13 > **Cross-harness fallback.** This skill scaffolds and tests packages via the Very Good CLI MCP server. On a host without this plugin's Bash hooks and without that MCP server connected, run the equivalent `very_good create dart_package …`, `very_good packages get`, and `very_good test` commands directly. ## Architecture Overview | Layer | Responsibility | Location | Depends On | Example | | --- | --- | --- | --- | --- | | **Data** | External communication — API calls, local storage, platform plugins | `packages/_api_client/` | External packages only | `user_api_client`, `local_storage_client` | | **Repository** | Data orchestration — combines data sources, transforms models, caches | `packages/_repository/` | Zero or more data layer packages | `user_repository`, `weather_repository` | | **Business Logic** | State management — processes user actions, emits state changes | `lib//bloc/` or `lib//cubit/` | Repository layer | `LoginBloc`, `ProfileCubit` | | **Presentation** | UI — widgets, pages, views, layout | `lib//view/` | Business Logic layer | `LoginPage`, `ProfileView` | ```text ┌─────────────────────────────────────────────┐ │ Presentation │ │ (lib//view/) │ └──────────────────┬──────────────────────────┘ │ reads state / dispatches events ┌──────────────────▼──────────────────────────┐ │ Business Logic │ │ (lib//bloc/) │ └──────────────────┬──────────────────────────┘ │ calls repository methods ┌──────────────────▼──────────────────────────┐ │ Repository │ │ (packages/_repository/) │ └──────────────────┬──────────────────────────┘ │ calls data clients ┌──────────────────▼──────────────────────────┐ │ Data │ │ (packages/_api_client/) │ └─────────────────────────────────────────────┘ ``` ## Monorepo Structure Features live in `lib/`, one directory each, split `bloc|cubit/` and `view/` with a barrel file. Layers live in `packages/`, one package per data source and one per repository. The second data package and the second repository follow the same shape as the first. ```text my_app/ ├── lib/ │ ├── app/ │ │ ├── app.dart # Barrel file │ │ └── view/ │ │ └── app.dart # App widget with MultiRepositoryProvider │ ├── login/ # Feature: login │ │ ├── login.dart # Barrel file │ │ ├── bloc/ │ │ │ ├── login_bloc.dart │ │ │ ├── login_event.dart │ │ │ └── login_state.dart │ │ └── view/ │ │ ├── login_page.dart # Page provides Bloc │ │ └── login_view.dart # View consumes state │ ├── profile/ # Feature: profile │ ├── main_development.dart # Flavor entrypoint │ ├── main_staging.dart │ └── main_production.dart ├── packages/ │ ├── auth_api_client/ # Data layer: auth API │ │ ├── lib/ │ │ │ ├── auth_api_client.dart # Barrel file │ │ │ └── src/ │ │ │ ├── auth_api_client.dart │ │ │ └── models/ │ │ │ ├── models.dart │ │ │ └── auth_response.dart │ │ └── pubspec.yaml │ ├── local_storage_client/ # Data layer: local storage │ ├── auth_repository/ # Repository layer: auth │ │ ├── lib/ │ │ │ ├── auth_repository.dart # Barrel file │ │ │ └── src/ │ │ │ ├── auth_repository.dart │ │ │ └── models/ │ │ │ ├── models.dart │ │ │ └── user.dart # Domain model │ │ └── pubspec.yaml │ └── user_repository/ # Repository layer: user ├── test/ │ └── ... # Mirrors lib/ structure └── pubspec.yaml # Root app pubspec ``` ## Data Layer The data layer handles all external communication. Each data package wraps a single external source (REST API, local database, platform plugin) and exposes typed methods and response models. **Rules:** - Models represent the external data shape — match the API/storage schema exactly - No Flutter imports — use the `very_good_cli` MCP server `create dart_package` tool - Constructor-inject HTTP clients for testability - Response models use `fromJson` / `toJson` factories - Export everything through a barrel file — never expose `src/` ### Pattern: Data Client Class Constructor-inject the HTTP client for testability. Return typed response models — never raw JSON. ```dart /// HTTP client for the User API. class UserApiClient { // http.Client injected — tests pass a mock, production gets a real client UserApiClient({ required String baseUrl, http.Client? httpClient, }) : _baseUrl = baseUrl, _httpClient = httpClient ?? http.Client(); final String _baseUrl; final http.Client _httpClient; /// Every method returns a typed response model. Future 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, ); } } ``` See [worked-example.md](references/worked-example.md) for the complete `user_api_client` package with pubspec, barrel files, response models, and exception class. ## Repository Layer The repository layer orchestrates data sources and exposes domain models. Each repository composes the data clients it needs, transforms response models into domain models, and provides a clean API for the business logic layer. **Rules:** - No inter-repository dependencies — repositories are isolated - No Flutter SDK — the `very_good_cli` MCP server `create dart_package` tool - Domain models live in the repository package — not in data packages - Transform data models into domain models — never leak API response shapes upstream - A repository with no external source, such as in-memory session state, takes no constructor arguments ### Pattern: Domain Model + Repository Transformation Domain models extend `Equatable` and represent the app's internal data shape — distinct from the API response shape. The repository method transforms between them. ```dart /// Domain model — lives in the repository package, NOT the data package. /// Fields match the app's needs, not the API schema. class const User({ required final String id, required final String email, required final String displayName, final String? avatarUrl, }) extends Equatable { @override List get props => [id, email, displayName, avatarUrl]; } ``` ```dart /// Repository accepts data client via constructor — never creates its own. class UserRepository { const UserRepository({ required UserApiClient userApiClient, }) : _userApiClient = userApiClient; final UserApiClient _userApiClient; /// Transforms UserResponse (API shape) → User (domain shape). Future getUser(String userId) async { final response = await _userApiClient.getUser(userId); return User( id: response.id, email: response.email, displayName: response.displayName, avatarUrl: response.avatarUrl, ); } } ``` See [worked-example.md](references/worked-example.md) for the complete `user_repository` package with pubspec, barrel files, and error handling. See [model-transformation.md](references/model-transformation.md) for detailed transformation patterns between data and domain models. ## Dependency Graph Path dependencies in each `pubspec.yaml` point one direction. A data package declares external packages only. A repository package declares a path dependency on its data packages. The root app declares its repository packages and every data package its bootstrap constructs. `main_.dart` imports each client to inject it, and `depend_on_referenced_packages` in `package:very_good_analysis` requires every imported package to be declared. **Imports hold the layer boundary.** Once the app declares a data package, the lint no longer stops a bloc from importing it. The boundary is the import rule in Core Standards: only the app's entrypoints and bootstrap import a data package. ```yaml # packages/user_api_client/pubspec.yaml — external packages only dependencies: http: ^1.4.0 # packages/user_repository/pubspec.yaml — path dependency on its data package dependencies: user_api_client: path: ../user_api_client # pubspec.yaml: repositories, plus the data packages bootstrap constructs dependencies: user_api_client: path: packages/user_api_client user_repository: path: packages/user_repository ``` See [references/pubspec.md](references/pubspec.md) for the three files in full and for checking the import boundary, including which files count as entrypoints and bootstrap. ## Data Flow Presentation dispatches an event → Bloc calls the repository → repository calls the data client and returns a domain model → `BlocBuilder` rebuilds on the new state. See [references/data-flow.md](references/data-flow.md) for the code at each layer. ## App Bootstrap `main_.dart` imports and constructs every data client and repository, then passes the repositories to the `App` widget, which exposes them through `MultiRepositoryProvider`. Flavors change only configuration — base URLs, API keys — never the wiring shape. ```dart // lib/main_development.dart import 'package:auth_api_client/auth_api_client.dart'; import 'package:auth_repository/auth_repository.dart'; import 'package:flutter/material.dart'; import 'package:my_app/app/app.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'; // Data layer final authApiClient = AuthApiClient(baseUrl: baseUrl); final userApiClient = UserApiClient(baseUrl: baseUrl); // Repository layer final authRepository = AuthRepository(authApiClient: authApiClient); final userRepository = UserRepository(userApiClient: userApiClient); runApp( App( authRepository: authRepository, userRepository: userRepository, ), ); } ``` See [references/worked-example.md](references/worked-example.md) for the full `main()` and the `App` widget with `MultiRepositoryProvider`. ## Anti-Patterns | Anti-Pattern | Problem | Correct Approach | | --- | --- | --- | | Widget calls API client directly | Bypasses Repository and Business Logic layers — no transformation, no state management | Widget dispatches event → Bloc calls Repository → Repository calls API client | | Repository imports another repository | Creates circular or tangled dependency graphs — breaks independent testability | Each repository is self-contained; combine data at the Bloc level if needed | | Domain models in data layer | Couples external API shape to internal domain — API changes break the entire app | Data layer has response models; Repository layer has domain models with transformation | | Business logic in repository | Repository becomes untestable monolith mixing orchestration with rules | Repository transforms data; Bloc/Cubit contains all business rules | | `git:` or pub version for local packages | Breaks monorepo — changes require publish/push cycles instead of instant local edits | Use `path:` dependencies for all packages within the monorepo | | Flutter imports in data/repository packages | Prevents packages from being used in Dart-only contexts (CLI tools, servers) | Scaffold with the `very_good_cli` MCP server `create dart_package` tool — no Flutter SDK dependency | | One giant repository for everything | God-object with too many responsibilities — impossible to test in isolation | One repository per domain boundary (`user_repository`, `settings_repository`) | | Importing `src/` directly | Breaks encapsulation — consumers depend on internal structure | Export public API through barrel files; import the package, never `src/` paths | ## Common Workflows ### Adding a New Data Source 1. Scaffold the package with the `very_good_cli` MCP server `create dart_package` tool: `_api_client --output-directory packages` 2. Add external dependencies to `pubspec.yaml` (e.g., `http`, `json_annotation`) 3. Create response models in `lib/src/models/` with `fromJson`/`toJson` 4. Create barrel file `lib/src/models/models.dart` exporting all models 5. Implement the client class in `lib/src/_api_client.dart` 6. Create the package barrel file `lib/_api_client.dart` exporting `src/` contents 7. Write unit tests in `test/` mirroring `lib/` structure — see the **testing** skill 8. Use `very_good_cli` MCP server tool `test` against the package directory — pass `directory: 'packages/_api_client'` to scope the run ### Adding a New Repository 1. Scaffold the package with the `very_good_cli` MCP server `create dart_package` tool: `_repository --output-directory packages` 2. Add path dependencies to data layer packages in `pubspec.yaml` 3. Add `equatable` to dependencies for domain models 4. Create domain models in `lib/src/models/` extending `Equatable` 5. Create barrel file `lib/src/models/models.dart` 6. Implement the repository class with constructor-injected data clients 7. Add transformation logic from response models to domain models 8. Create the package barrel file `lib/_repository.dart` 9. Write unit tests with mocked data clients — see the **testing** skill ### Connecting a Repository to a Feature 1. Add path dependencies on the repository package and on each data package it takes to root `pubspec.yaml` 2. Create the data clients and the repository in `main_.dart` and pass the repository to `App` 3. Add `RepositoryProvider.value` in `App`'s `MultiRepositoryProvider` 4. Create the Bloc/Cubit with the repository injected — see the **bloc** skill 5. Build the Page/View with `BlocProvider` and `BlocBuilder` — see the **bloc** skill 6. Grep `lib/` and `test/` for `package:/` imports. The work is done when every hit is an entrypoint or bootstrap file, as the pubspec reference's Import Boundary section defines them ## Additional Resources - [Complete worked example](references/worked-example.md) and [pubspec reference](references/pubspec.md) - [Data flow walkthrough](references/data-flow.md) — the request path with code at each layer - [Model transformation patterns](references/model-transformation.md) — data model vs domain model conversion - [Package-level testing](references/testing.md) — testing data clients and repositories in isolation - For Bloc/Cubit patterns and Page/View separation — see the **bloc** skill - For project scaffolding use the `very_good_cli` MCP server `create dart_package` tool - For testing data clients, repositories, and Blocs — see the **testing** skill