Skip to content
Back to archipelago.banuacoder.com

Scaffolding

app_scaffold Free ​

Tier
Freeversion 2.1.4
Command
archipelago generate app_scaffold
Generates
  • apps/<app_name>

Generate a new app in an existing Archipelago v2 workspace. Creates the full app shell with DI, router, bootstrap, flavors, Fastlane, launcher icons, and splash screen configuration.

Variables ​

VariableTypeDefaultDescription
appNamestringMyAppThe name of the application
organizationstringMyCompanyYour organization or company name
domainstringcomYour organization top-level domain. See App and Bundle IDs for how they become the Android and iOS IDs.
prefixstringAppUnique prefix for component naming

Install ​

Interactive ​

bash
archipelago generate app_scaffold

Non-interactive (CI) ​

bash
archipelago generate app_scaffold --config my_config.json

Run this inside an existing Archipelago workspace (one already scaffolded by flutter_modular_monorepo) to add a second (or later) app — the UI kit, shared packages, and infrastructure already need to exist. post_gen scans packages/ for the sole *_ui_kit directory and rewrites the rendered app_ui_kit import/dependency in pubspec.yaml and every Dart file under lib/ to match it, aborting loudly if it finds zero or more than one candidate.

post_gen also adds apps/<app_name> to the workspace, to flavors.yaml, to the IDE run configurations (.vscode/launch.json, .vscode/tasks.json, .run/, merged with the other apps; see IDE run configurations), and to target_paths in build_prepare.yaml. Without the last one, build-prepare --flavor production and --check-leaks skip the new app (1.6.0 and later). The new app has no debug tooling of its own; add app_debugger or app_blackbox to it by hand if you need them. It does depend on the workspace's features/app_marionette (Marionette MCP in debug builds only, swapped out by build-prepare --mode release|profile), which flutter_modular_monorepo 5.0.0 and later generates (there is no brick of its own). In an older workspace pre_gen stops with "Nothing was changed": copy features/app_marionette/ from a fresh project (archipelago create --output <scratch dir>) and run the brick again. post_gen then adds the Marionette debug_build_only entries to build_prepare.yaml when they are missing.

After generation, run dart pub get, then dart run monorepo_toolkit flavor generate --app <app_name> from the workspace root, then dart run build_runner build in apps/<app_name>. The toolkit runs every flavorizr instruction in the app's flavorizr.yaml without prompting. The new app's RunnerUITests target (Patrol on iOS) is added before the flavors exist, so it has only the Debug, Release and Profile configurations; flavored schemes still list it.

Config Template ​

json
{
  "@appName": "The name of the application",
  "appName": "MyApp",
  "@organization": "Your organization or company name",
  "organization": "MyCompany",
  "@domain": "Your organization top-level domain",
  "domain": "com",
  "@prefix": "Unique prefix for component naming",
  "prefix": "App"
}

Generated Structure ​

text
apps/
└── {app_name}/
    ├── .well-known/
    │   ├── apple-app-site-association
    │   └── assetlinks.json
    ├── android/app/                       # build.gradle.kts, proguard-rules.pro, MainActivity.kt
    ├── ios/Runner/AppDelegate.swift
    ├── fastlane/
    │   ├── Appfile
    │   ├── Fastfile
    │   ├── Matchfile
    │   └── Pluginfile
    ├── launcher/                          # App icons per flavor
    │   ├── ios-logo.png
    │   ├── ios-logo-dev.png
    │   ├── ios-logo-stg.png
    │   ├── logo.png
    │   ├── logo-dev.png
    │   ├── logo-stg.png
    │   ├── logo-background.png
    │   ├── logo-background-dev.png
    │   ├── logo-background-stg.png
    │   └── logo-foreground.png
    ├── splash/                            # Splash screen assets
    ├── lib/
    │   ├── app.dart
    │   ├── bootstrap.dart
    │   ├── main.dart
    │   ├── main_development.dart
    │   ├── main_staging.dart
    │   ├── di/injector.dart
    │   ├── host/
    │   │   ├── host_app_widget.dart
    │   │   ├── services/deeplink_service.dart
    │   │   └── initializers/
    │   │       ├── initializer.dart
    │   │       ├── initializer_runner.dart
    │   │       ├── pre_launch_initializer.dart
    │   │       └── post_launch_initializer.dart
    │   └── router/app_router.dart
    ├── integration_test/                  # Patrol/integration_test scaffolding
    ├── test/
    │   ├── architecture/feature_boundary_test.dart  # No feature depends on another's _impl
    │   ├── di/injector_test.dart          # Composition-root smoke test
    │   ├── di/{lazy_feature_sdk,base_scope_budget,port_singleton}_test.dart  # DI guard tests
    │   └── support/composition_root_harness.dart, feature_files.dart  # Harness filled by leaf bricks
    ├── Gemfile
    ├── flavorizr.yaml
    ├── flutter_launcher_icons-development.yaml
    ├── flutter_launcher_icons-production.yaml
    ├── flutter_launcher_icons-staging.yaml
    ├── flutter_native_splash.yaml
    └── pubspec.yaml

Public API ​

app.dart is the MaterialApp.router root: it takes an AppRouter and applies {prefix}Theme.light()/.dark(). bootstrap.dart runs the layered error-handling setup (Flutter framework errors, platform-dispatcher errors, runZonedGuarded), installs consent (ConsentInitializer.initialize(GetIt.I), so the ConsentGate is resolvable and telemetry is not stuck fail-closed), leaves an archipelago:firebase:init marker region for Firebase-based bricks, restores the saved locale, calls initializeInjector(), then runApps a HostAppWidget:

dart
Future<void> bootstrap({
  required FlavorStatus flavor,
  required AppBuilder builder,
}) => runZonedGuarded(() async {
  // ... error handlers, ConsentInitializer.initialize(), AppLocaleController.restore(), initializeInjector() ...
  runApp(HostAppWidget(
    initializerController: PreLaunchInitializerController.create(),
    builder: (context, result) => builder(result),
    loadingWidget: const _BootstrapLoadingIndicator(),
  ));
}, (error, stackTrace) { /* uncaught async errors */ });

HostAppWidget drives the two-phase initialization lifecycle: pre-launch runs in initState and blocks the UI (showing loadingWidget; loading and error states render inside a themed MaterialApp shell and the error state is retryable) until PreLaunchInitializerController.create().initialize() resolves; initializer_runner.dart applies per-initializer timeouts and criticality and reports failures to AppMonitoring; the pre-launch sweep seals FeatureSDKRegistry and only initializes enabled SDKs. The first post-frame callback then runs PostLaunchInitializer.create(context:, router:) non-blockingly. For the launch trace's native milestones, bootstrap() calls markDartMain() first and AppDelegate calls LaunchTimeTrackerPlugin.markAppInitStart() inside #if canImport(app_launch_tracker_impl); Android needs no host code (see launch_tracker_sdk). AppRouter extends RootStackRouter composes routes purely from RouteRegistry._registry.routes — feature SDKs register their own routes during pre-launch, so this file never lists routes directly.

DI & Wiring ​

di/injector.dart is the @InjectableInit composition root, registering AppUtilitiesPackageModule and RouterRegistryPackageModule as external modules before the archipelago:feature_modules_before:/archipelago:feature_modules_after: marker pairs that feature bricks inject into. archipelago:feature_imports: is the matching import marker pair.

test/di/injector_test.dart runs initializeInjector() under flutter test. Its only setup is installCompositionRootPrerequisites() from test/support/composition_root_harness.dart. A new app starts with an empty harness. A brick that you install into the app later adds its own test prerequisite to the harness. See Testing.

The app also ships the DI guard tests of flutter_modular_monorepo, identical apart from the app name: feature boundary, lazy SDKs, boot budget and port identity. See DI Guard Tests.

Dependencies ​

Generated app pubspec.yaml: get_it ^9.2.1, flutter_native_splash ^2.4.8, plus workspace path dependencies including consent_impl, locale_core and app_launch_tracker_impl; dev: flutter_launcher_icons ^0.14.4, flutter_flavorizr ^2.5.0, change_app_package_name ^1.5.0, patrol ^4.8.0, integration_test (SDK), auto_route_generator ^10.5.0, injectable_generator ^3.0.2, logger ^2.7.0 (used by injector_test.dart).

Platform Setup ​

post_gen runs flutter create in a scratch directory and merges the result into android//ios/ (backfilling Podfile, Runner.xcodeproj, the Gradle wrapper, AndroidManifest.xml — files this brick doesn't template itself) via shared_hooks' PlatformScaffoldHelper. The merge only adds files missing from the target; it never overwrites this brick's own customized files (android/app/build.gradle.kts, ios/Runner/AppDelegate.swift, android/app/proguard-rules.pro). If flutter create fails (e.g. flutter not on PATH), generation still succeeds and prints the exact manual flutter create apps/<name> --org <org> command to run instead. It also generates a debug keystore and computes the SHA-256 fingerprint for .well-known/assetlinks.json (advisory — a failing keytool just leaves the placeholder untouched).

Android Release Build ​

android/app/build.gradle.kts's release build type ships with isMinifyEnabled/isShrinkResources on and proguardFiles(...) wired to android/app/proguard-rules.pro. That file carries baseline @Keep-respecting and stack-trace-preserving rules, plus an archipelago:proguard:keep-rules marker pair — capability bricks (webview_sdk, webview_sdk_advanced, biometric_auth_sdk) append their own conditional keep rules there at generation time, so a release build only carries the rules the app's actual dependency tree needs.

An archiveDebugSymbols Gradle task is also registered:

bash
./gradlew :app:archiveDebugSymbols -Pvariant=release

Run after a release build — copies that variant's R8 mapping.txt and native-debug-symbols bundle into build/outputs/debug-artifacts/<variant>/, a stable path CI or a developer can upload to a crash-reporting vendor for deobfuscation.

Limitations ​

  • MaterialApp.router has an empty archipelago:app_builder marker region for its builder: parameter; app_debugger fills it with the debug overlay (1.6.0 and later).
  • router.config(...) passes navigatorObservers: () => [...] with an empty archipelago:navigator_observers region; journey_tracker adds its observer there (2.0.0 and later).
  • AppRouter.initialPath ships as a hardcoded '/' with a TODO — auth/introduction gating logic is left to the developer.

Changelog ​

Run archipelago brick info app_scaffold to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.