Skip to content

Architecture

Migrating feature DI ​

Notes for projects generated before October 2026. New projects already have this shape.

Upgrade shared/dependencies first ​

The newer bricks import barrels that an older shared/dependencies lacks, and they expect the new shared/dependencies_core package. Run the shared_dependencies brick before the steps below:

bash
archipelago generate shared_dependencies -c deps.json --on-conflict overwrite

deps.json holds your stateManagement (and appName). A project from before the core package gets the core package only on the first run, and shared/dependencies is left as it is ("Nothing was changed" for it). Run the command again with ARCHIPELAGO_FORCE=1 if you did not change shared/dependencies, or merge it with archipelago upgrade shared_dependencies if you did. The result:

  • shared/dependencies_core exists, with the Flutter-free barrels bloc.dart (bloc and cubit projects), di.dart, equality.dart and meta.dart.
  • shared/dependencies depends on it and has a new meta.dart barrel next to state.dart, di.dart, equality.dart and auto_route.dart.
  • Your own *_api packages can depend on dependencies_core instead of dependencies.

Without a terminal (CI, scripts) --on-conflict overwrite or skip answers the overwrite prompt, and --config stands in for the config prompt; see the CLI reference.

Feature manifests and lazy SDKs ​

shared/feature_sdk now separates what a feature declares at boot (FeatureManifest) from what it does (its SDK). Features that must exist at boot keep a FeatureSDK; the rest register a const manifest and a lazy SDK.

1. Update shared/feature_sdk ​

Copy lib/src/feature_manifest.dart, lib/src/feature_sdk.dart, lib/src/feature_sdk_registry.dart, lib/src/feature_container.dart and lib/feature_sdk.dart from a freshly generated project (or from template_base/shared/feature_sdk), and add dependencies: {path: ../dependencies} to its pubspec.yaml: the barrel exports FeatureContainer, which imports package:dependencies/di.dart. Then rename the registry accessors everywhere:

BeforeAfter
FeatureSDKRegistry.instance.sdksFeatureSDKRegistry.instance.manifests
FeatureSDKRegistry.instance.enabledSdksFeatureSDKRegistry.instance.enabledManifests
FeatureSDKRegistry.instance.findModuleForPage(id) (older projects)findManifestForPage(id)
findSdkForPage(id)findManifestForPage(id)
FeatureSDKRegistry.instance.getEnabledPageConfigurations() (older projects)FeatureSDKRegistry.instance.enabledPageConfigurations (a getter, no parentheses)

In apps/<app>/lib/host/initializers/pre_launch_initializer.dart and host_app_widget.dart, loop over enabledManifests/manifests and type the loop variable as FeatureManifest. Every existing FeatureSDK is a FeatureManifest, so features you have not migrated keep working.

2. Migrate a feature (optional, per feature) ​

For a feature whose initialize() does no pre-first-frame work:

  1. Add lib/src/<name>_manifest.dart with class const XManifest() extends FeatureManifest. Move registerRoutes, pageConfigurations, isEnabled and any InitialPathProvider claim into it. It must not resolve from GetIt in its constructor, isEnabled or registerRoutes.

  2. Make the _api port a plain interface: drop implements FeatureSDK, keep launch() and the destinations.

  3. In the SDK impl, drop the create() factory and the FeatureSDKRegistry.register call. Move any initialize() setup into the constructor or first use; move dispose() cleanup to a dispose: callback on the registration.

  4. In the global module:

    dart
    FeatureSDKRegistry.instance.register(const XManifest());
    gh.lazySingleton<XSDK>(XSDKImpl.new);
  5. Run melos run build, melos run analyze and test/di/injector_test.dart. A useful guard: expect(getIt.checkLazySingletonInstanceExists<XSDK>(), isFalse) after initializeInjector().

Updated starter bricks (home_sdk, paywall_sdk, introduction_sdk, ai_chat_sdk) generate this shape and need the new shared/feature_sdk from step 1.

Every brick whose generated code or tests read FeatureSDKRegistry.instance.manifests checks for it first: on a project that still has sdks, it stops with "Nothing was changed" and a link here. Those bricks are the four starters above, app_debugger and its panel bricks (app_debugger_analytics, _feature_flag, _miniapp, _network, _storage, _websocket), app_blackbox_recorder, connectivity_sdk, shorebird_sdk, webview_sdk_advanced and feature_monorepo_skeleton. Do step 1, then run the brick again.

ai_chat_sdk 2.0.0: AiChatSDK no longer extends FeatureSDK (it keeps launch()), and AiChatSDKImpl takes no initialMessages; it loads the stored history itself on first use and emits it on conversationStream. Code that read currentMessages right after resolving the SDK should listen to conversationStream instead.

Per-feature containers ​

Features generated before this change push a named scope onto GetIt.instance from lib/src/di/<name>_local_module.dart. That still works. To move a feature to its own container:

  1. Make sure shared/feature_sdk exports FeatureContainer (copy it from a fresh project if not).

  2. Add lib/src/di/<name>_imports.dart:

    dart
    void import<Name>Globals(GetIt child, GetIt root) {
      child.registerFactory<HttpClient>(() => root<HttpClient>());
    }

    Forward every root type your repositories, datasources and use cases take. Anything not forwarded now throws StateError instead of falling back to the root.

  3. Add lib/src/di/<name>_container.dart:

    dart
    final <name>Container = FeatureContainer(
      '<name>',
      root: GetIt.instance,
      build: (child, root) {
        import<Name>Globals(child, root);
        init<Name>LocalDI(child);
      },
    );
  4. In the shell page, replace init<Name>LocalModule()/reset<Name>LocalModule() with a lease: _lease = <name>Container.acquire() in initState, build after _lease.ready, unawaited(_lease.release().catchError(...)) in dispose.

  5. Replace <name>GetIt<T>() in pages with constructor parameters the shell fills from _lease.get<T>(). Then delete <name>_local_module.dart.

  6. If the injector used ignoreUnregisteredTypes for types that came from the root through scope fallback, keep them there and forward them in the imports file.

  7. Run melos run build, melos run analyze and the feature's tests. A useful guard: build the container from a fresh GetIt.asNewInstance() holding only the imported types, take a lease, and expect lease.get<SomethingNotImported> to throw.

A port the feature owns goes into the root once, from its global module with a // di-global: <reason> comment. Other features (and the feature itself) import it, so all of them share one instance.

auth and paywall (auth_sdk 3.0.0, paywall_sdk 2.0.0) ​

The starter bricks now generate auth and paywall with a container too. To move an existing project by hand, follow the steps above for each, with these specifics:

  • auth: auth_imports.dart forwards LoginRepository and AuthSDK. Keep LoginRepository in ignoreUnregisteredTypes. AuthImplGlobalModule does not change: AuthSDK, the repository and the token store stay in the root because they restore the session and refresh tokens with no auth screen mounted. AuthShellPage builds LoginCubit/RegisterCubit (or the bloc, provider or riverpod equivalents) from the lease above the AutoRouter; the pages read them from context instead of loginDependencies()/registerDependencies().
  • paywall: paywall_imports.dart forwards PaywallSDK (the lazy root singleton). PaywallShellPage builds the state holder from the lease and wraps the router in PaywallScope(paywallSDK: _lease.get<PaywallSDK>()); PaywallPage reads PaywallScope.of(context).paywallSDK instead of paywallDependencies().
  • riverpod: the notifiers take their dependencies by constructor. Declare the provider with a creator that throws and dependencies: const [], and override it in the shell: ProviderScope(overrides: [loginNotifierProvider.overrideWith(() => LoginNotifier(...))], child: const AutoRouter()).
  • Delete auth_local_module.dart, paywall_local_module.dart and their tests. Copy test/src/di/auth_di_test.dart and test/src/di/paywall_di_test.dart from a fresh project.