Skip to content

Understanding FeatureSDK Self-Registration

What you'll learn

  • The FeatureSDK base 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:

dart
// 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);
}
MethodPurpose
registerRoutesAdds the feature's AutoRoute entries to the app router
initializePerforms async setup after route registration (state restoration, listeners)
disposeCleans up resources on app shutdown
launchEntry 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:

dart
// 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:

dart
// 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:

dart
// 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:

dart
// 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:

dart
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 AppRouter

Adding a new feature

With self-registration, adding a feature requires:

  1. Create the feature packages (feature_api/, feature_impl/)
  2. Implement FeatureSDK with a create() factory that calls FeatureSDKRegistry.instance.register(this)
  3. Create a global module that registers the SDK as an eager singleton via GetIt.instance.registerSingleton()
  4. Add the module to externalPackageModulesBefore in the app's injector.dart

No router file to update, no startup sequence to modify. The feature discovers itself.

Next steps

Built by Banua Coder