Core Scaffolding
How to add a new feature module to a Flutter monorepo
Free Reference for feature_monorepo_skeleton: every variable, option and package it generates. Command: archipelago generate feature_monorepo_skeleton
Create a feature module in an Archipelago monorepo: pick single package or API/Impl, wire it into the host app, then add routes, DI and logic layers.
What you'll learn
- Creating a new feature module inside your monorepo
- Choosing between single-package and API/Impl split patterns
- Wiring the feature into the host app through a manifest and a lazy SDK
- Adding routes, DI, and business logic layers
Prerequisites
- An existing Archipelago monorepo (see Monorepo Scaffolding)
- Understanding of clean architecture layers (data, domain, presentation)
Step 1: Generate the Feature
From your monorepo root, run:
archipelago generate feature_monorepo_skeletonAnswer the prompts:
- featureName:
Payment(PascalCase, becomespaymentin paths) - isShared:
falsefor a self-contained feature,trueif other features depend on it - hasBusinessLogic:
trueto include repos, usecases, and DI layers - hasLocale:
trueif the feature manages its own translations - isPlugin:
trueif the feature needs native Android and iOS code. It addsandroid/,ios/and a DartMethodChannelwrapper (see the brick reference)
Step 2: Non-Interactive Config
{
"featureName": "Payment",
"isShared": false,
"hasBusinessLogic": true,
"hasLocale": true,
"isPlugin": false,
"includeGenerated": false
}archipelago generate feature_monorepo_skeleton --config payment_config.jsonStep 3: Single-Package Structure
When isShared: false, you get a self-contained feature:
features/
└── payment/
├── lib/
│ ├── payment.dart # Barrel export
│ └── src/
│ ├── payment_manifest.dart # Routes, read at boot
│ ├── payment_sdk.dart # Port + lazy implementation
│ ├── data/
│ │ ├── datasources/
│ │ │ ├── local/
│ │ │ └── remote/
│ │ ├── models/
│ │ └── repositories/
│ ├── di/
│ │ ├── injector.dart # Generated local DI
│ │ ├── payment_container.dart # paymentContainer (child GetIt)
│ │ ├── payment_global_module.dart # Manifest + lazy SDK
│ │ └── payment_imports.dart # Root types the feature uses
│ ├── domain/
│ │ ├── entities/
│ │ ├── repositories/
│ │ └── usecases/
│ ├── presentation/
│ │ ├── payment_shell_page.dart
│ │ └── ui/pages/
│ └── router/
│ └── payment_router.dart
├── test/di/payment_di_test.dart # Lazy SDK + container isolation
└── l10n/ # Localization filesStep 4: API/Impl Split Pattern
Set isShared: true when the feature exposes contracts to other features (e.g., auth status checks). This generates two packages:
features/
├── payment_api/ # Port (plain interface) and shared models
│ └── lib/src/
│ └── payment_sdk.dart
└── payment_impl/ # Full implementation
└── lib/src/
├── payment_manifest.dart
├── payment_sdk_impl.dart
├── data/
├── di/
├── domain/
├── presentation/
└── router/Other features depend on payment_api only, never on payment_impl. The host app wires the impl at the DI level.
Step 5: Check the Registration
You do not register the feature by hand. The brick's post_gen hook:
- adds the package(s) to the workspace,
- adds the feature (
payment_implfor the shared variant) to the app'spubspec.yaml, - registers
PaymentGlobalModule(PaymentImplGlobalModulefor the shared variant) in the app'slib/di/injector.dart, and declares it underarchipelago: di_modules:in the feature pubspec.
The global module registers PaymentManifest with FeatureSDKRegistry, so its routes exist before the router is built. It registers PaymentSDK as a lazy singleton. PaymentSDKImpl is built only when something resolves it. Run dart pub get and melos run build to generate DI and routes. Then run flutter test test/di in the feature to check the lazy SDK and the container isolation.
Step 6: Add Dependencies
Repositories, datasources and use cases live in the feature's own container, not the app root:
Annotate them with
@injectableor@lazySingleton. Then build_runner adds them toinitPaymentLocalDI.For each app-root type they take (a network client, storage), add one line to
payment_imports.dart:dartchild.registerFactory<HttpClient>(() => root<HttpClient>());In
PaymentShellPage, pass what the state holder needs from the lease:dartcreate: (_) => PaymentCubit(_lease.get<LoadPaymentsUseCase>())..load(),
The container never falls back to the root, so a type you forgot to import fails loudly instead of resolving another feature's registration.
State Management Variable
The feature_monorepo_skeleton brick accepts a stateManagement variable that generates a state management skeleton alongside the feature module structure.
Options: bloc / cubit / riverpod / provider / none
| Value | Generated files |
|---|---|
cubit | _cubit.dart, _state.dart, _cubit_test.dart |
bloc | _bloc.dart, _event.dart, _state.dart, _bloc_test.dart |
riverpod | _notifier.dart, _state.dart, _notifier_test.dart |
provider | _notifier.dart, _notifier_test.dart |
none | No state layer is generated. The existing ValueNotifier presentation shell stays |
Example: generate a feature with a cubit:
archipelago generate feature_monorepo_skeleton --featureName Payment --stateManagement cubitChoosing none leaves the default ValueNotifier in the presentation shell untouched. This is the safest choice when you want to add state management later or prefer to manage it yourself.
For more detail on each variant, see the state_management brick tutorial.
When to Use Each Pattern
| Pattern | Use When |
|---|---|
| Single package | Feature is self-contained, no other module needs its contracts |
| API/Impl split | Other features depend on this feature's types or status |
| No business logic | Pure UI feature (e.g., onboarding slides, static about page) |
| With locale | Feature has user-facing strings that need translation |
Next Steps
- Set up authentication with the Auth SDK brick
- Learn about the monitoring SDK and the noop pattern