with one click
vgv-layered-architecture
// Best practices for VGV layered monorepo architecture in Flutter.
// Best practices for VGV layered monorepo architecture in Flutter.
Audit or remediate Flutter widgets against WCAG 2.2 accessibility conformance levels A, AA, or AAA across iOS, Android, Web, macOS, Windows, and Linux.
VGV-specific reference for bumping Dart and Flutter SDK constraints across packages. Covers pubspec.yaml environment constraints, CI workflow Flutter versions, and SDK upgrade PR preparation. CI uses ^MAJOR.MINOR.x to resolve to the latest patch; pubspec pins the exact patch version (e.g., ^3.50.1).
Best practices for Flutter animations using the built-in animation framework. Use when creating, modifying, or reviewing animations, transitions, motion, or animated widgets. Covers implicit animations, explicit animations, page transitions, and Material 3 motion tokens.
Best practices for Dart unit tests, Flutter widget tests, and golden file tests.
Scaffold a new Dart or Flutter project from a Very Good CLI template. Supports flutter_app, dart_package, flutter_package, flutter_plugin, dart_cli, flame_game, and docs_site templates.
Audits package dependency licenses using the Very Good CLI packages_check_licenses MCP tool. Flags non-compliant or unknown licenses and produces a compliance summary.
| name | vgv-layered-architecture |
| description | Best practices for VGV layered monorepo architecture in Flutter. |
| when_to_use | Use when structuring a multi-package Flutter app, creating data or repository packages, defining layer boundaries, or wiring dependencies between packages. |
| allowed-tools | Read Glob Grep mcp__very-good-cli__create mcp__very-good-cli__packages_get mcp__very-good-cli__test |
| effort | high |
Layered monorepo architecture for Flutter apps ā four layers organized as independent Dart packages with strict unidirectional dependencies.
Apply these standards to ALL layered architecture work:
packages/ ā each is an independent Dart package with its own pubspec.yamllib/ ā organized by feature within the appvery_good_cli MCP server create dart_package tooluser_repository, weather_repository, auth_repositorygit: or pub version references for packages in the same reposrc/ is never imported directly by consumersmain_<flavor>.dart creates clients and repositories, provides them via RepositoryProvider| 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/ | 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/) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
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
ā ā āāā profile.dart
ā ā āāā cubit/
ā ā ā āāā profile_cubit.dart
ā ā ā āāā profile_state.dart
ā ā āāā view/
ā ā āāā profile_page.dart
ā ā āāā profile_view.dart
ā āāā 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
ā ā āāā lib/
ā ā ā āāā local_storage_client.dart
ā ā ā āāā src/
ā ā ā āāā local_storage_client.dart
ā ā āāā pubspec.yaml
ā āāā 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
ā āāā lib/
ā ā āāā user_repository.dart
ā ā āāā src/
ā ā āāā user_repository.dart
ā ā āāā models/
ā ā āāā models.dart
ā ā āāā user_profile.dart
ā āāā pubspec.yaml
āāā test/
ā āāā ... # Mirrors lib/ structure
āāā pubspec.yaml # Root app pubspec
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:
very_good_cli MCP server create dart_package toolfromJson / toJson factoriessrc/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.
The repository layer orchestrates data sources and exposes domain models. Each repository composes one or more data clients, transforms response models into domain models, and provides a clean API for the business logic layer.
Rules:
very_good_cli MCP server create dart_package toolDomain 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 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 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.
Each layer's pubspec.yaml enforces the architecture through path dependencies.
packages/user_api_client/pubspec.yaml)dependencies:
# External packages only ā no local dependencies
http: ^1.4.0
json_annotation: ^4.9.0
packages/user_repository/pubspec.yaml)dependencies:
equatable: ^2.0.7
# Path dependency on data layer package
user_api_client:
path: ../user_api_client
pubspec.yaml)dependencies:
flutter:
sdk: flutter
flutter_bloc: ^9.1.0
# Repository packages only ā data packages are transitive
auth_repository:
path: packages/auth_repository
user_repository:
path: packages/user_repository
The app never depends on data packages directly. Data packages are transitive dependencies through repositories. This enforces the layer boundary ā business logic and presentation cannot bypass the repository layer.
Step-by-step walkthrough: user taps "Load Profile" button.
context.read<ProfileBloc>().add(ProfileLoadRequested(userId: '123'))_userRepository.getUser(event.userId) and emits state based on the resultUserRepository.getUser delegates to _userApiClient.getUser and transforms the response into a domain UserUserApiClient.getUser makes the HTTP request and returns a typed UserResponseBlocBuilder based on the new state// 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());
}
}
See data-flow.md for the full data flow walkthrough with code at each layer.
The app's main_<flavor>.dart creates all data clients and repositories, then passes them to the App widget. MultiRepositoryProvider makes repositories available to the entire widget tree.
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';
// Data layer
final authApiClient = AuthApiClient(baseUrl: baseUrl);
final userApiClient = UserApiClient(baseUrl: baseUrl);
final localStorageClient = LocalStorageClient();
// Repository layer
final authRepository = AuthRepository(
authApiClient: authApiClient,
localStorageClient: localStorageClient,
);
final userRepository = UserRepository(
userApiClient: userApiClient,
);
runApp(
App(
authRepository: authRepository,
userRepository: userRepository,
),
);
}
Flavors change only the configuration (base URLs, API keys) ā the architecture stays identical across development, staging, and production. See worked-example.md for the App widget with MultiRepositoryProvider.
| 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 |
very_good_cli MCP server create dart_package tool: <name>_api_client --output-directory packagespubspec.yaml (e.g., http, json_annotation)lib/src/models/ with fromJson/toJsonlib/src/models/models.dart exporting all modelslib/src/<name>_api_client.dartlib/<name>_api_client.dart exporting src/ contentstest/ mirroring lib/ structure ā see the testing skillvery_good_cli MCP server tool test against the package directory ā pass directory: 'packages/<name>_api_client' to scope the runvery_good_cli MCP server create dart_package tool: <name>_repository --output-directory packagespubspec.yamlequatable to dependencies for domain modelslib/src/models/ extending Equatablelib/src/models/models.dartlib/<name>_repository.dartpubspec.yamlmain_<flavor>.dart and pass it to AppRepositoryProvider.value in App's MultiRepositoryProviderBlocProvider and BlocBuilder ā see the bloc skillvery_good_cli MCP server create dart_package tool