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:
archipelago generate shared_dependencies -c deps.json --on-conflict overwritedeps.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_coreexists, with the Flutter-free barrelsbloc.dart(bloc and cubit projects),di.dart,equality.dartandmeta.dart.shared/dependenciesdepends on it and has a newmeta.dartbarrel next tostate.dart,di.dart,equality.dartandauto_route.dart.- Your own
*_apipackages can depend ondependencies_coreinstead ofdependencies.
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:
| Before | After |
|---|---|
FeatureSDKRegistry.instance.sdks | FeatureSDKRegistry.instance.manifests |
FeatureSDKRegistry.instance.enabledSdks | FeatureSDKRegistry.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:
Add
lib/src/<name>_manifest.dartwithclass const XManifest() extends FeatureManifest. MoveregisterRoutes,pageConfigurations,isEnabledand anyInitialPathProviderclaim into it. It must not resolve from GetIt in its constructor,isEnabledorregisterRoutes.Make the
_apiport a plain interface: dropimplements FeatureSDK, keeplaunch()and the destinations.In the SDK impl, drop the
create()factory and theFeatureSDKRegistry.registercall. Move anyinitialize()setup into the constructor or first use; movedispose()cleanup to adispose:callback on the registration.In the global module:
dartFeatureSDKRegistry.instance.register(const XManifest()); gh.lazySingleton<XSDK>(XSDKImpl.new);Run
melos run build,melos run analyzeandtest/di/injector_test.dart. A useful guard:expect(getIt.checkLazySingletonInstanceExists<XSDK>(), isFalse)afterinitializeInjector().
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:
Make sure
shared/feature_sdkexportsFeatureContainer(copy it from a fresh project if not).Add
lib/src/di/<name>_imports.dart:dartvoid 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
StateErrorinstead of falling back to the root.Add
lib/src/di/<name>_container.dart:dartfinal <name>Container = FeatureContainer( '<name>', root: GetIt.instance, build: (child, root) { import<Name>Globals(child, root); init<Name>LocalDI(child); }, );In the shell page, replace
init<Name>LocalModule()/reset<Name>LocalModule()with a lease:_lease = <name>Container.acquire()ininitState, build after_lease.ready,unawaited(_lease.release().catchError(...))indispose.Replace
<name>GetIt<T>()in pages with constructor parameters the shell fills from_lease.get<T>(). Then delete<name>_local_module.dart.If the injector used
ignoreUnregisteredTypesfor types that came from the root through scope fallback, keep them there and forward them in the imports file.Run
melos run build,melos run analyzeand the feature's tests. A useful guard: build the container from a freshGetIt.asNewInstance()holding only the imported types, take a lease, and expectlease.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.dartforwardsLoginRepositoryandAuthSDK. KeepLoginRepositoryinignoreUnregisteredTypes.AuthImplGlobalModuledoes 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.AuthShellPagebuildsLoginCubit/RegisterCubit(or the bloc, provider or riverpod equivalents) from the lease above theAutoRouter; the pages read them from context instead ofloginDependencies()/registerDependencies(). - paywall:
paywall_imports.dartforwardsPaywallSDK(the lazy root singleton).PaywallShellPagebuilds the state holder from the lease and wraps the router inPaywallScope(paywallSDK: _lease.get<PaywallSDK>());PaywallPagereadsPaywallScope.of(context).paywallSDKinstead ofpaywallDependencies(). - 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.dartand their tests. Copytest/src/di/auth_di_test.dartandtest/src/di/paywall_di_test.dartfrom a fresh project.