Dual-GetIt DI Pattern
What you'll learn
- Why Archipelago uses two GetIt instances (global + local per feature)
- How infrastructure SDKs register in the global scope
- How features create isolated local scopes via shell pages
- The lifecycle: mount → register → use → dispose → reset
The Problem
In a monorepo with many feature modules, registering everything in a single DI container causes:
- Naming collisions between features
- Leaked state when navigating between features
- Tight coupling — features can accidentally depend on each other's internals
The Solution: Global + Local GetIt
Archipelago uses two GetIt scopes:
Global Scope (GetIt.instance)
Shared infrastructure available everywhere — registered once at app startup.
dart
// apps/template_app/lib/di/injector.dart
final getIt = GetIt.instance;
@InjectableInit(
externalPackageModulesBefore: [
ExternalModule(AppUtilitiesPackageModule),
ExternalModule(NetworkSdkDioPackageModule),
ExternalModule(MonitoringImplPackageModule),
ExternalModule(RouterRegistryPackageModule),
// Feature SDKs (eager — triggers self-registration)
ExternalModule(AuthImplGlobalModule),
ExternalModule(HomeImplGlobalModule),
],
)
Future<void> initializeInjector() => $initGetIt(getIt);Infrastructure packages expose their module via @InjectableInit.microPackage():
dart
// infrastructure/network_sdk_dio/lib/src/di/injector.dart
@InjectableInit.microPackage()
void initMicroPackage(GetIt getIt) => $initMicroPackage(getIt);Local Scope (GetIt.asNewInstance() per feature)
Each feature creates its own isolated GetIt instance for internal deps (repos, datasources, usecases):
dart
// features/auth/auth_impl/lib/src/di/auth_local_module.dart
final GetIt authGetIt = GetIt.asNewInstance();
void initAuthLocalModule() => initAuthLocalDI(authGetIt);
void resetAuthLocalModule() => authGetIt.reset();Shell Page Lifecycle
The local DI scope is tied to the feature's shell page — a StatefulWidget that wraps all nested routes:
dart
// features/auth/auth_impl/lib/src/presentation/auth_shell_page.dart
@RoutePage()
class AuthShellPage extends StatefulWidget { ... }
class _AuthShellPageState extends State<AuthShellPage> {
@override
void initState() {
super.initState();
initAuthLocalModule(); // Register local deps
}
@override
void dispose() {
resetAuthLocalModule(); // Reset for clean re-entry
super.dispose();
}
@override
Widget build(BuildContext context) => const AutoRouter();
}When the user navigates to /auth, the shell mounts and registers local deps. When they navigate away, dispose() resets the container for a clean slate.
Accessing Dependencies
dart
// Global deps — available anywhere
final networkClient = getIt<NetworkClient>();
final authSDK = getIt<AuthSDK>();
// Local deps — only within the feature
final loginUseCase = authGetIt<LoginUseCase>();
final loginRepo = authGetIt<LoginRepository>();When to Use Each Scope
| Scope | Register | Examples |
|---|---|---|
| Global | App startup (eager singletons) | SDKs, network, monitoring, auth state |
| Local | Shell page initState() | Repos, datasources, usecases, BLoCs |
Key Rules
- FeatureSDKs are global — registered as eager singletons so
FeatureSDKRegistryis populated before pre-launch - Internal deps are local — repos, datasources, usecases stay in the feature's own GetIt
- Local deps can access global —
authGetItcan resolve fromgetItif needed viaGetIt.instance - Reset on dispose —
authGetIt.reset()ensures no leaked state between feature visits
Next Steps
- FeatureSDK Registration — how SDKs self-register
- Two-Phase Initialization — the boot sequence