Scaffolding
feature_monorepo_skeleton Free
- Tier
- Freeversion 2.1.0
- Command
archipelago generate feature_monorepo_skeleton- Generates
- one empty feature package, or an _api and _impl pair, named after featureName
Scaffolds a new, empty feature module into an existing Archipelago monorepo — a single package, or a shared
_api/_implsplit — with optional business logic (DI, repositories, use cases), state management, per-feature localization, and native Android and iOS code (a Flutter plugin). This is what you run to start a brand-new feature from scratch; the finished feature SDK bricks (auth_sdk,rating_review_sdk, etc.) are hand-written, richer examples of the same shape this brick scaffolds bare.
Variables
| Variable | Type | Default | Description |
|---|---|---|---|
| featureName | string | FeatureName | The name of the feature module |
| isShared | boolean | false | Whether this is a shared feature (API/Impl split) |
| hasBusinessLogic | boolean | true | Whether this feature has business logic (repos, usecases, DI) |
| hasLocale | boolean | false | Whether this feature manages its own localization |
| isPlugin | boolean | false | Whether this feature carries native Android and iOS code (a Flutter plugin). Always both platforms |
| includeGenerated | boolean | false | Whether to include generated files in version control |
| stateManagement | enum (bloc, cubit, riverpod, provider, none) | cubit | State management solution for the feature |
| prefix | string | App | UI kit class prefix; the shell page uses locale_core's <prefix>Locale. pre_gen reads it from archipelago.yaml (project.prefix), so you rarely set it |
pre_gen.dart derives isBloc/isCubit/isRiverpod/isProvider/isNone/usesBloc/usesProvider/usesRiverpod/hasStateManagement from stateManagement, the plugin's pluginOrg (<domain>.<organization> from archipelago.yaml, com.example without one) and pluginOrgPath, plus a fixed slang config (l10nBaseLocale: en, l10nInputDirectory: l10n, l10nOutputDirectory: lib/src/l10n, ...) used to keep slang.yaml and build.yaml's slang_build_runner: options in sync when hasLocale is on.
Usage
Interactive
archipelago generate feature_monorepo_skeletonNon-interactive (CI)
archipelago generate feature_monorepo_skeleton --config my_config.jsonGenerated Structure
Single package (isShared: false)
features/
└── {feature_name}/
├── lib/
│ ├── {feature_name}.dart # Barrel export
│ └── src/
│ ├── {feature_name}_manifest.dart # Routes, read at boot
│ ├── {feature_name}_sdk.dart # Port + lazy implementation
│ ├── data/{datasources,models,repositories}/ # if hasBusinessLogic
│ ├── di/{feature_name}_global_module.dart # Manifest + lazy SDK into the root
│ ├── di/{feature_name}_container.dart, {feature_name}_imports.dart, injector.dart # if hasBusinessLogic
│ ├── domain/{entities,repositories,usecases}/ # if hasBusinessLogic
│ ├── presentation/{feature_name}_shell_page.dart, ui/{pages,widgets}/
│ └── router/{feature_name}_router.dart
│ └── platform/{feature_name}_platform_channel.dart # if isPlugin
├── test/di/{feature_name}_di_test.dart # Lazy SDK, container isolation and teardown
├── test/platform/{feature_name}_platform_channel_test.dart # if isPlugin
├── android/ # if isPlugin: build.gradle, kotlin/<org>/{feature_name}/{FeatureName}Plugin.kt
├── ios/ # if isPlugin: {feature_name}.podspec, Classes/{FeatureName}Plugin.swift
├── l10n/ # if hasLocale — en.i18n.json, id.i18n.json
├── slang.yaml # if hasLocale
├── analysis_options.yaml
├── build.yaml
└── pubspec.yamlAPI/Impl split (isShared: true)
features/
├── {feature_name}_api/
│ └── lib/src/{feature_name}_sdk.dart # port only, no FeatureSDK
└── {feature_name}_impl/
├── lib/src/{feature_name}_manifest.dart, {feature_name}_sdk_impl.dart, data/, di/, domain/, presentation/, router/
├── lib/src/platform/ # if isPlugin
├── test/di/{feature_name}_di_test.dart
├── test/platform/ # if isPlugin
├── android/, ios/ # if isPlugin: native code lives only here, _api stays pure Dart
├── l10n/, slang.yaml # if hasLocale
└── pubspec.yamlWhichever variant is generated always lands flat at features/<name>/... (or features/<name>_api/ + features/<name>_impl/) — never double-nested under an extra <name>/<name>/ directory (fixed in 1.2.0).
What gets scaffolded
- Every feature gets a const
<Name>Manifest(its routes) and a lazy SDK. The global module registers the manifest inFeatureSDKRegistryandgh.lazySingleton<<Name>SDK>(<Name>SDKImpl.new), so the feature is not built at boot. The_apiport is a plain interface withlaunch(); a port method that returns data usesFuture<T?>. hasBusinessLogic: trueadds the feature's own container:<name>_container.dart:<name>Container = FeatureContainer('<name>', root: GetIt.instance, build: build<Name>Container). The build runs the imports, then the generatedinit<Name>LocalDI(child).<name>_imports.dart:import<Name>Globals(child, root), the only file that reads the app root. It starts empty; add onechild.registerFactory<T>(() => root<T>())line per root type the feature uses. The container never falls back to the root, so anything else throws.- The shell page takes a lease in
initState, builds its content afterlease.ready, and releases the lease indispose(errors go toFlutterError.reportError). It creates the Cubit/Bloc/Notifier and passes it what it needs from_lease.get<T>(). NoGetIt.instanceappears outsidelib/src/di/. - A port the feature owns and other features use is registered once, in the feature's global module, with a
// di-global: <reason>comment; the feature reads it back through its imports file.
hasBusinessLogic: falseproduces a UI-only feature: nodata/,domain/, container or imports — the global module (manifest + lazy SDK), presentation and router stay.test/di/<name>_di_test.dartchecks that the global module registers the manifest and that the SDK is not built until resolved, and (with business logic) that the container cannot resolve a root type it did not import, and that it builds from a fake root holding only the imported types, resolves every registration, and is empty with every closable singleton closed after the last release. Add one fake to that root per type you forward in<name>_imports.dart. See DI Guard Tests.stateManagementcleans up every state-management variant's files except the chosen one via_cleanupStateManagementFilesinpost_gen.dart;noneremoves state-management files entirely.hasLocale: truescaffoldsl10n/en.i18n.json+l10n/id.i18n.json, aslang.yaml, and ahasLocale-gatedslang_build_runner:legacystanza inbuild.yaml(the defaultslang_build_runneris turned off, see flutter_l10n) plus theslang/slang_flutter/slang_build_runnerdependencies (bothsingle_featureandshared_featurereached parity here as of 1.2.0/1.1.2) — rundart run slang(ormelos run build) afterward to generate the translations class.- post_gen wires the feature into the app: it adds the package (
<name>_implfor the shared variant) to the app'spubspec.yamland registers<Name>GlobalModule/<Name>ImplGlobalModuleinlib/di/injector.dart(fixed in 1.3.2). - The UI kit dependency uses the project's real UI kit name (
<app>_ui_kit, fromarchipelago.yaml), notapp_ui_kit. State classes usefreezed ^4.0.1, which resolves with analyzer 13 (fixed in 1.3.2). isPlugin: truemakes the feature package a Flutter plugin for Android and iOS. With the API/Impl split, that is<name>_impl;<name>_apistays pure Dart.pubspec.yamldeclaresflutter: plugin: platforms:withpluginClass: <Name>Pluginfor both platforms andpackage: <org>.<package>for Android.- Android:
android/build.gradlereads AGP, Kotlin and SDK versions fromgradle/libs.versions.tomlat the workspace root (post_gen writes it when a project has none), and<Name>Plugin.ktis aFlutterPluginandMethodCallHandler. - iOS:
ios/<package>.podspecandios/Classes/<Name>Plugin.swift. - Both sides listen on one
MethodChannelnamed<org>/<package>, for examplecom.banua_coder/cart, where<org>is the project's<domain>.<organization>. A samplegetPlatformVersionmethod returnsAndroid <release>oriOS <version>. - Dart:
lib/src/platform/<name>_platform_channel.dartwraps the channel and returnsnullwhen the plugin is not registered. WithhasBusinessLogicit is a@lazySingletonin the feature container, so a repository takes it by constructor like any datasource.test/platform/<name>_platform_channel_test.dartfakes the channel withTestDefaultBinaryMessenger. - Add a native call by adding a
casein the Kotlin and Swiftwhen/switchand a method on the Dart wrapper.
includeGenerated: false(default) keeps generated files out of version control (adds them to.gitignore); settrueto commit them.
Dependencies
feature_sdk,router_registry,identifiers,dependencies— shared workspace packages (only referenced whenhasBusinessLogic/routing code is generated).- No self-generation and no hard dependency on another brick — this is a leaf scaffolding brick.
Known Limitations
- Native code (
isPlugin) covers Android and iOS only, always both. The template targets mobile. - The plugin is a single package with a
MethodChannel. Federated plugins (one package per platform) and FFI are not scaffolded; write them by hand if you need them. - The plugin's Android namespace is
<org>.<package>, so a feature named like the app collides with the app's own namespace. Pick another feature name. - The plugin has no Swift Package Manager manifest (
Package.swift), soflutter build ioslists it among plugins without Swift Package Manager support. CocoaPods builds it. - Renaming a feature after generation is a manual file/identifier rename; there's no
archipelago renamecommand. - Only one state-management choice per feature; mixing (e.g. one screen on Riverpod, another on Bloc) isn't scaffolded.
Changelog
Run archipelago brick info feature_monorepo_skeleton to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.