Understanding FeatureSDK Self-Registration
What you'll learn
- The
FeatureSDKbase class contract and its 4 methods - How features self-register using
FeatureSDKRegistry - The
AuthSDKImpl.create()factory pattern - How eager singletons in global modules trigger registration
- How
RouteRegistry.lock()prevents late route additions
The problem
In a modular monorepo, adding a new feature typically requires touching the app layer in multiple places: adding routes to the router, wiring up DI, adding initialization calls. With many features, this creates a central bottleneck.
The solution: self-registration
Each feature registers itself with FeatureSDKRegistry when its SDK is instantiated. The app discovers features automatically during pre-launch initialization.
The FeatureSDK contract
Every feature implements this base class from shared/feature_sdk:
// shared/feature_sdk/lib/src/feature_sdk.dart
abstract class FeatureSDK {
void registerRoutes(RouteRegistry registry);
Future<void> initialize();
Future<void> dispose();
Future<void> launch(BuildContext context);
}| Method | Purpose |
|---|---|
registerRoutes | Adds the feature's AutoRoute entries to the app router |
initialize | Performs async setup after route registration (state restoration, listeners) |
dispose | Cleans up resources on app shutdown |
launch | Entry point for navigating to the feature (push route, manage flow) |
The registry
FeatureSDKRegistry is a singleton that collects all feature SDKs. It prevents duplicate registration and supports unregistration for cleanup:
// shared/feature_sdk/lib/src/feature_sdk_registry.dart
class FeatureSDKRegistry {
FeatureSDKRegistry._();
static final FeatureSDKRegistry instance = FeatureSDKRegistry._();
final List<FeatureSDK> _sdks = [];
List<FeatureSDK> get sdks => List.unmodifiable(_sdks);
void register(FeatureSDK sdk) {
if (_sdks.contains(sdk)) {
throw StateError('FeatureSDK ${sdk.runtimeType} is already registered.');
}
_sdks.add(sdk);
}
void unregister(FeatureSDK sdk) => _sdks.remove(sdk);
void clear() => _sdks.clear();
}Implementing a feature SDK
Concrete SDKs use a private constructor with a create() factory that handles self-registration:
// features/auth/auth_impl/lib/src/auth_sdk_impl.dart
class AuthSDKImpl implements AuthSDK {
AuthSDKImpl._();
static AuthSDKImpl create() {
final sdk = AuthSDKImpl._();
FeatureSDKRegistry.instance.register(sdk);
return sdk;
}
@override
void registerRoutes(RouteRegistry registry) {
registry.register([
AutoRoute(
page: AuthShellRoute.page,
path: '/auth',
children: [
AutoRoute(page: LoginRoute.page, path: 'login', initial: true),
AutoRoute(page: RegisterRoute.page, path: 'register'),
],
),
]);
}
@override
Future<void> initialize() async {
_currentStatus = AuthStatus.unauthenticated;
_statusController.add(_currentStatus);
}
@override
Future<void> dispose() async {
await _statusController.close();
_eventListeners.clear();
}
@override
Future<void> launch(BuildContext context) => launchAuthFlow(context);
}The private constructor (AuthSDKImpl._()) ensures that create() is the only way to instantiate the SDK, guaranteeing self-registration always happens.
Eager singleton in global module
The SDK is registered as an eager singleton in the feature's global DI module:
// features/auth/auth_impl/lib/src/di/auth_global_module.dart
class AuthImplGlobalModule extends MicroPackageModule {
@override
FutureOr<void> init(GetItHelper gh) {
GetIt.instance.registerSingleton<AuthSDK>(AuthSDKImpl.create());
}
}This module is listed in the app's @InjectableInit.externalPackageModulesBefore. When initializeInjector() runs, it calls init() on each module. AuthSDKImpl.create() is called immediately (eager), which triggers FeatureSDKRegistry.instance.register(this).
Why not gh.singleton() or gh.lazySingleton()? The injectable helper's gh.singleton() uses lazy registration internally. With lazy registration, the SDK would not be created until first access, but PreLaunchInitializer reads the registry before anyone accesses AuthSDK. The registry would be empty.
RouteRegistry lock pattern
RouteRegistry uses a lock mechanism to prevent routes from being added after the router is built:
// shared/router_registry/lib/src/route_registry.dart
abstract class RouteRegistry {
void register(List<AutoRoute> routes);
List<AutoRoute> get routes;
bool get isLocked;
void lock();
}During pre-launch, after all SDKs have called registerRoutes(), the registry is locked:
routeRegistry.lock();Any attempt to register routes after locking throws a StateError. This catches a common bug: a lazy singleton trying to register routes after the router is already built.
Registration flow
1. bootstrap.dart calls initializeInjector()
2. Global DI processes externalPackageModulesBefore in order:
|- AppUtilitiesPackageModule
|- NetworkSdkDioPackageModule
|- ...
|- AuthImplGlobalModule.init()
| \- GetIt.instance.registerSingleton<AuthSDK>(AuthSDKImpl.create())
| \- FeatureSDKRegistry.instance.register(this)
|- HomeImplGlobalModule.init()
| \- same pattern
\- PaywallImplGlobalModule.init()
3. runApp(HostAppWidget(...))
4. PreLaunchInitializer reads FeatureSDKRegistry.instance.sdks
5. Calls sdk.registerRoutes(routeRegistry) for each
6. routeRegistry.lock()
7. Calls sdk.initialize() for each
8. Returns InitializerResult with AppRouterAdding a new feature
With self-registration, adding a feature requires:
- Create the feature packages (
feature_api/,feature_impl/) - Implement
FeatureSDKwith acreate()factory that callsFeatureSDKRegistry.instance.register(this) - Create a global module that registers the SDK as an eager singleton via
GetIt.instance.registerSingleton() - Add the module to
externalPackageModulesBeforein the app'sinjector.dart
No router file to update, no startup sequence to modify. The feature discovers itself.
Next steps
- GetIt Dependency Injection -- How global DI and microPackage modules work
- Two-Phase Initialization -- How the boot sequence orchestrates registration and launch