Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion references/ffca/data.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The different storage mechanisms are known as _data sources_. The repository is

For example, to enable some offline browsing, our online store app may have a Drift database data source and a Shopify http api data source. These live within the `data` package of a feature.

The mealify feature-first app does exactly this. Its `meals_data` package holds two data sources side by side: a [Drift database](https://github.com/VGVentures/mealify_feature_first/tree/main/features/meals/meals_data/lib/src/data_sources/meals_database) for local storage, and an [http client](https://github.com/VGVentures/mealify_feature_first/tree/main/features/meals/meals_data/lib/src/data_sources/mealdb_api_client) for the remote API. Neither is visible outside the package: the [repository](https://github.com/VGVentures/mealify_feature_first/blob/main/features/meals/meals_data/lib/src/repositories/meals_repository.dart) combines them, and the domain layer only ever sees a `Meal`.
The mealify feature-first app does exactly this. Its `meals_data` package holds two data sources side by side: a [Drift database](https://github.com/VGVentures/mealify_feature_first/tree/main/features/meals/meals_data/lib/src/data_sources/meals_database) for local storage, and an [http client](https://github.com/VGVentures/mealify_feature_first/tree/main/features/meals/meals_data/lib/src/data_sources/mealdb_api_client) for the remote API. The package's barrel exports both so the app can construct them and inject them into the repository, but it must not export DTOs or mappers. The [repository](https://github.com/VGVentures/mealify_feature_first/blob/main/features/meals/meals_data/lib/src/repositories/meals_repository.dart) converts the `ApiMeal` and `DbMeal` DTOs to a `Meal` object, and the other layers never see them.

## DTOs and mappers

Expand Down
2 changes: 1 addition & 1 deletion references/ffca/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Notice that the `user_profile` feature depends on `auth_domain`, and `auth` know
Generate a Dart package that knows how to talk to the Swagger client in the `shared` folder. As part of the package, it should start with a Dart script in the `tool` folder. The script should:

1. Download the latest version of the OpenAPI or Swagger spec.
2. Generate the client with a package like [swagger_to_dart](https://pub.dev/packages/swagger_to_dart). It uses [Dio](https://pub.dev/packages/dio), so you get the same HTTP client as the `api_client` recipe in the [Very Good Flutter Cookbook](https://github.com/VGVentures/very_good_flutter_cookbook).
2. Generate the client with a package like [swagger_to_dart](https://pub.dev/packages/swagger_to_dart), which uses [Dio](https://pub.dev/packages/dio) under the hood.
3. Export the parts each feature needs.

Each feature then uses that shared client inside its own `data` package. Given a `/products/{id}` endpoint returning an `ApiProduct` DTO, `product_data` exposes a `ProductsRepository` with a `getProductById` method that:
Expand Down
9 changes: 5 additions & 4 deletions references/ffca/presentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ A widget that owns a `Cubit` is not a separate feature. It lives in its own feat
`CartBadge` depends on `cart_domain`, its own feature's domain. What makes it worth discussing is that it renders inside a screen owned by a different feature. The question is not whether a widget may reach across domains. It is who places it, and what the dependency costs.

```dart
// cart_presentation/lib/cart_badge/bloc/cart_badge_cubit.dart
// cart_presentation/lib/src/cart_badge/bloc/cart_badge_cubit.dart

class CartBadgeCubit extends Cubit<CartBadgeState> {
CartBadgeCubit({required ICartsRepository cartsRepository})
Expand Down Expand Up @@ -176,7 +176,7 @@ Use a direct import behind a narrow barrel when the widget is genuinely part of

When the same widget is wanted in more than one feature, work through this in order.

1. **Does it need a repository or a domain type?** If not, it is a pure presentational component, and it belongs in `ui_kit`. This test is mechanical rather than stylistic. [Shared packages depend on external packages only](/architecture/ffca/project_structure/#dependency-rules), so a widget that needs a repository cannot compile there.
1. **Does it need a repository or a domain type?** If not, it is a pure presentational component, and it belongs in `ui_kit`. This test is mechanical rather than stylistic. [Shared packages never depend on feature packages](/architecture/ffca/project_structure/#dependency-rules), so a widget that needs a repository cannot compile there.
2. **If it does, split it.** `CartBadgeView`, taking a plain count, goes in `ui_kit`. `CartBadge`, the `Cubit` plus that view, stays in `cart_presentation` behind its own barrel. In practice, a widget that gets genuinely reused across features is usually a presentational leaf with the stateful part left behind, which means the reuse pressure was pointing at `ui_kit` all along.

It becomes its own feature when it grows its own business logic, and a [presentation-only feature](/architecture/ffca/overview/#presentation-only-features) when it composes several features without owning business logic of its own.
Expand All @@ -188,8 +188,9 @@ A single feature may expose multiple independent entry points. For example, `fav
- features/favorites/
- favorites_presentation/
- lib/
- favorites_list/
- favorites_detail/
- src/
- favorites_list/
- favorites_detail/
- favorites_list.dart (subfeature barrel)
- favorites_detail.dart (subfeature barrel)
- favorites_presentation.dart (primary barrel, re-exports all)
Expand Down
59 changes: 32 additions & 27 deletions references/ffca/project_structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ This table is the whole rule, and it is the version a validation script implemen
| `{feature}_domain` | shared packages, other features' `_domain` |
| `{feature}_data` | its own `_domain`, other features' `_domain`, shared packages |
| `{feature}_presentation` | its own `_domain`, other features' `_domain`, other features' `_presentation`, shared packages |
| `shared` | external packages only |
| `shared` | other shared packages, external packages |

Two absences from that table carry as much weight as the rows themselves:

- **No presentation package depends on any `_data` package**, its own included. The app wires the data layer implementation in.
- **No shared package depends on a feature package.** A shared package depends on external packages only, which is why a widget that needs a repository cannot live in `ui_kit`.
- **No shared package depends on a feature package.** A shared package may depend on other shared packages and external packages, but never on a feature package, which is why a widget that needs a repository cannot live in `ui_kit`.

Cycles between packages are forbidden at every layer. If two packages each need something from the other, that shared part belongs in a third package underneath both of them.

Expand Down Expand Up @@ -109,33 +109,36 @@ Data packages with a backend suffix, such as `auth_data_firebase`, signal the ba
- product/
- product_domain/ Dart package
- lib/
- models/
- product.dart
- repositories/
- i_products_repository.dart
- src/
- models/
- product.dart
- repositories/
- i_products_repository.dart
- product_domain.dart barrel file
- product_data/ Dart package
- lib/
- data_sources/
- products_remote_data_source/
- dtos/
- product_dto.dart
- products_remote_data_source.dart
- mappers/
- product_mapper.dart
- repositories/
- products_repository.dart
- src/
- data_sources/
- products_remote_data_source/
- dtos/
- product_dto.dart
- products_remote_data_source.dart
- mappers/
- product_mapper.dart
- repositories/
- products_repository.dart
- product_data.dart barrel file
- product_presentation/ Flutter package
- lib/
- product_detail/
- bloc/
- views/
- product_detail_module.dart
- product_list/
- bloc/
- views/
- product_list_module.dart
- src/
- product_detail/
- bloc/
- views/
- product_detail_module.dart
- product_list/
- bloc/
- views/
- product_list_module.dart
- product_detail.dart subfeature barrel
- product_list.dart subfeature barrel
- product_presentation.dart primary barrel
Expand Down Expand Up @@ -164,7 +167,7 @@ Data packages with a backend suffix, such as `auth_data_firebase`, signal the ba

Each layer uses the same subfolders every time, so you always know where to look for something.

**Domain** (`{feature}_domain/lib/`):
**Domain** (`{feature}_domain/lib/src/`):

| Folder | Contents |
| --------------- | ----------------------------------------------------------------------------- |
Expand All @@ -174,23 +177,25 @@ Each layer uses the same subfolders every time, so you always know where to look

One note on naming: the folder keeps the conventional `use_cases/` name so the layout matches other clean architecture projects, even though we [name the classes themselves](/architecture/ffca/domain/#business-rules) Command and Query.

**Data** (`{feature}_data/lib/`):
**Data** (`{feature}_data/lib/src/`):

| Folder | Contents |
| --------------- | -------------------------------------------------------------------- |
| `data_sources/` | Remote and local data sources, each with a `dtos/` subfolder |
| `mappers/` | Extension methods mapping DTOs and generated classes to domain models |
| `repositories/` | Concrete repository implementations |

**Presentation** (`{feature}_presentation/lib/`):
**Presentation** (`{feature}_presentation/lib/src/`):

| Folder | Contents |
| --------------------------------------------- | --------------------------------------------------- |
| `{screen_name}/bloc/` | `Bloc` or `Cubit`, plus state and event classes |
| `{screen_name}/views/` | Screen and widget implementations |
| `{screen_name}/{screen_name}_module.dart` | The module wiring dependencies for that screen |

Each layer has a primary barrel file at the root of `lib/`, named `{feature}_{layer}.dart`, plus [subfeature barrel files](/architecture/ffca/presentation/#subfeature-barrel-files) for any entry point that should be independently importable.
Each layer has a primary barrel file at the root of `lib/`, named `{feature}_{layer}.dart`, plus [subfeature barrel files](/architecture/ffca/presentation/#subfeature-barrel-files) for any entry point that should be independently importable. Everything else lives under `lib/src/`, so it stays private by default. Packages import each other only through the barrel files at the root of `lib/`, never a file inside `lib/src/`. See [barrel files](/architecture/barrel_files/) for the general rule.

The data barrel exports the repository and the data sources, so the app can construct the data sources and inject them into the repository. It never exports DTOs or mappers.

## Monorepo tooling

Expand Down
17 changes: 9 additions & 8 deletions scripts/validate_layers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -502,13 +502,13 @@ List<_Violation> _checkOwnership(Package p, Workspace ws) {
}

/// Checks the package's barrels and its imports and exports of other
/// workspace packages. Implements the barrels in "Folder layout" and "Layer
/// subfolders" and the dedicated-barrel condition in "Dependency rules" of
/// references/ffca/project_structure.md, and "Widgets that own state" in
/// references/ffca/presentation.md ("never the primary barrel"). The general
/// rule that packages import each other only through barrels is on the VGV
/// Engineering barrel files page, which the FFCA section does not state yet
/// (VGVentures/very_good_engineering#294). Imports go through the same rules
/// workspace packages. Implements "Layer subfolders" in
/// references/ffca/project_structure.md (the primary barrel, packages import
/// each other only through the barrels at the root of lib/, and the data
/// barrel never exports DTOs or mappers), the dedicated-barrel condition in
/// its "Dependency rules", and "Widgets that own state" in
/// references/ffca/presentation.md ("never the primary barrel"). Imports go
/// through the same rules
/// table as pubspec dependencies, since a workspace resolves an undeclared
/// import of another member. In [incremental] mode a package with no lib/ yet
/// is being scaffolded, so its missing barrel is not reported until --all.
Expand Down Expand Up @@ -550,7 +550,8 @@ List<_Violation> _checkImports(
for (final f in p.dartFiles) {
// Any barrel at the root of a data package's lib/ that exports its own
// DTOs or mappers, directly or through one re-exporting file. Data
// sources are exported: the app constructs them to inject them.
// sources are exported: the app constructs them to inject them ("Layer
// subfolders" in references/ffca/project_structure.md).
final rootBarrel =
f.libRel.startsWith('lib/') && !f.libRel.substring(4).contains('/');
if (p.layer == 'data' && rootBarrel) {
Expand Down
2 changes: 1 addition & 1 deletion skills/ffca-architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ These are the violations the architecture exists to prevent. Read the cited sect
- Presentation importing a data layer. Depend on the domain and go through its repository interface. See `references/ffca/project_structure.md`, section *Dependency rules*.
- A shared package depending on a feature. See `references/ffca/overview.md`, section *Shared libraries*.
- Business logic living in `shared/`. Volatile, app-specific logic belongs in a feature domain. See the component table in the same section.
- A widget in `ui_kit` that needs a repository. It cannot compile there, because shared packages depend on external packages only. Split it: the presentational view goes to `ui_kit`, the stateful part stays in its feature. See `references/ffca/presentation.md`, section *Where the widget should live*.
- A widget in `ui_kit` that needs a repository. It cannot compile there, because shared packages never depend on feature packages. Split it: the presentational view goes to `ui_kit`, the stateful part stays in its feature. See `references/ffca/presentation.md`, section *Where the widget should live*.

## Where to go next

Expand Down
2 changes: 1 addition & 1 deletion skills/ffca-cross-feature/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ A widget that owns a `Cubit` is not a separate feature. It lives in its own feat

When the same widget is wanted in more than one feature, work through `references/ffca/presentation.md`, section *Where the widget should live*, in order:

1. **Does it need a repository or a domain type?** If not, it is a pure presentational component and belongs in `ui_kit`. This test is mechanical, not stylistic: shared packages depend on external packages only, so a widget that needs a repository cannot compile there.
1. **Does it need a repository or a domain type?** If not, it is a pure presentational component and belongs in `ui_kit`. This test is mechanical, not stylistic: shared packages never depend on feature packages, so a widget that needs a repository cannot compile there.
2. **If it does, split it.** The view taking a plain count goes in `ui_kit`; the Cubit plus that view stays in its feature behind its own barrel. A widget that genuinely gets reused across features is usually a presentational leaf with the stateful part left behind, which means the pressure was pointing at `ui_kit` all along.

## When a Query is not enough: compose a feature
Expand Down
Loading