SuperAPI AI Directory

layered-architecture

General & other

View repository

Updated 10-16 01:28

About this skill

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_<flavor>.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/<name>_api_client/ External packages only user_api_client, local_storage_client
Repository Data orchestration — combines data sources, transforms models, caches packages/<name>_repository/ Zero or more data layer packages user_repository, weather_repository
Business Logic State management — processes user actions, emits state changes lib/<feature>/bloc/ or lib/<feature>/cubit/ Repository layer LoginBloc, ProfileCubit
Presentation UI — widgets, pages, views, layout lib/<feature>/view/ Business Logic layer LoginPage, ProfileView
┌─────────────────────────────────────────────┐
│              Presentation                   │
│          (lib/<feature>/view/)              │
└──────────────────┬──────────────────────────┘
                   │ reads state / dispatches events
┌──────────────────▼──────────────────────────┐
│            Business Logic                   │
│        (lib/<feature>/bloc/)                │
└──────────────────┬──────────────────────────┘
                   │ calls repository methods
┌──────────────────▼──────────────────────────┐
│              Repository                     │
│      (packages/<name>_repository/)          │
└──────────────────┬──────────────────────────┘
                   │ calls data clients
┌──────────────────▼──────────────────────────┐
│               Data                          │
│     (packages/<name>_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.

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.

/// 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<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>,
    );
  }
}

See 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.

/// 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<Object?> get props => [id, email, displayName, avatarUrl];
}
/// 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<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,
    );
  }
}

See worked-example.md for the complete user_repository package with pubspec, barrel files, and error handling. See 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_<flavor>.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.

# 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 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 for the code at each layer.

App Bootstrap

main_<flavor>.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.

// 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 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: <name>_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/<name>_api_client.dart
  6. Create the package barrel file lib/<name>_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/<name>_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: <name>_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/<name>_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_<flavor>.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:<data_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