๐ Dependency Overrides โ
Replace, extend, or intercept dependency registrations at any level of the module graph.
Simple Overrides โ
Pass an overrides callback that runs after binds() but before exports():
// On ModuleController (Dart):
final controller = ModuleController(
NetworkModule(),
overrides: (binder) {
binder.registerSingleton<ApiService>(FakeApiService());
},
);
// On ModuleScope (Flutter):
ModuleScope(
module: NetworkModule(),
overrides: (binder) {
binder.registerSingleton<ApiService>(FakeApiService());
},
child: const NetworkPage(),
)TIP
Simple overrides only affect the root module's binder. To override bindings inside imported modules, use ModuleOverrideScope.
ModuleOverrideScope โ
A hierarchical tree that maps module types to override callbacks, targeting specific modules in the import graph.
Structure โ
final overrideScope = ModuleOverrideScope(
selfOverrides: (binder) { ... }, // Override root module
children: {
AuthModule: ModuleOverrideScope(
selfOverrides: (binder) { ... }, // Override AuthModule
),
DataModule: ModuleOverrideScope(
selfOverrides: (binder) { ... }, // Override DataModule
children: {
CacheModule: ModuleOverrideScope(
selfOverrides: (binder) { ... },
),
},
),
},
);Usage โ
final overrideScope = ModuleOverrideScope(
children: {
AuthModule: ModuleOverrideScope(
selfOverrides: (binder) {
binder.registerLazySingleton<AuthService>(() => FakeAuthService());
},
),
},
);
// On ModuleController:
final controller = ModuleController(
AppModule(),
overrideScopeTree: overrideScope,
);
// On ModuleScope (Flutter):
ModuleScope(
module: AppModule(),
overrideScope: overrideScope,
child: const AppPage(),
)Composing Scopes โ
withAdditionalOverride() -- chains an override after existing selfOverrides:
final extended = baseScope.withAdditionalOverride((binder) {
binder.registerSingleton<int>(42);
});merge() -- combines two scopes; self overrides compose in order, children merge recursively:
final merged = scopeA.merge(scopeB);
// scopeA overrides run first, then scopeB
// Overlapping children are merged recursivelyOverride Timing โ
INFO
Overrides run between binds() and exports(), so they replace private registrations before export. Dependencies resolved via binder.get<T>() in exports() pick up the overridden instances.
Hot Reload
During hotReload(), overrides are re-applied with the same timing -- no additional setup needed.
Interceptors โ
ModuleInterceptor provides lifecycle hooks for cross-cutting concerns:
class TimingInterceptor implements ModuleInterceptor {
final _timers = <Type, Stopwatch>{};
@override
void onInit(Module module) {
_timers[module.runtimeType] = Stopwatch()..start();
}
@override
void onLoaded(Module module) {
final elapsed = _timers[module.runtimeType]?.elapsed;
print('${module.runtimeType} loaded in $elapsed');
}
@override
void onError(Module module, Object error) {
print('${module.runtimeType} failed: $error');
}
@override
void onDispose(Module module) {
_timers.remove(module.runtimeType);
}
}| Event | When |
|---|---|
onInit(module) | Before initialization starts |
onLoaded(module) | After onInit() completes successfully |
onError(module, error) | When initialization throws |
onDispose(module) | When the module is disposed |
Per-controller vs Global โ
// Per-controller:
ModuleController(MyModule(), interceptors: [TimingInterceptor()]);
// Global (Flutter) -- applied to all ModuleScope widgets:
void main() {
runApp(ModularityRoot(
interceptors: [TimingInterceptor()],
child: MyApp(),
));
}Lifecycle Logging โ
Built-in logging for module retention events (creation, reuse, disposal, cache operations). Pass lifecycleLogger to ModularityRoot:
// Enable console logging:
ModularityRoot(
lifecycleLogger: ModularityRoot.defaultDebugLogger,
child: const MyApp(),
)
// Custom logger:
ModularityRoot(
lifecycleLogger: (event, moduleType, {retentionKey, details}) {
myLogger.info('${event.name}: $moduleType key=$retentionKey');
},
child: const MyApp(),
)
// Disable (default -- omit lifecycleLogger):
ModularityRoot(
child: const MyApp(),
)Output example:
[Modularity] CREATED ProfileModule key=ProfileModule-/profile
[Modularity] REUSED ProfileModule key=ProfileModule-/profile
[Modularity] EVICTED ProfileModule key=ProfileModule-/profileModuleLifecycleEvent enum values
| Event | Description |
|---|---|
created | Controller created for the first time |
reused | Existing controller reused from cache |
registered | Controller registered in retention cache |
disposed | Controller disposed |
evicted | Controller evicted from retention cache |
released | Controller released (ref count decremented) |
routeTerminated | Route termination triggered controller cleanup |
Common Use Cases โ
Feature flags โ
ModuleScope(
module: PaymentModule(),
overrides: (binder) {
if (FeatureFlags.newCheckout) {
binder.registerLazySingleton<CheckoutFlow>(() => NewCheckoutFlow());
}
},
child: const CheckoutPage(),
)Environment-specific DI โ
ModuleController(
AppModule(),
overrides: (binder) {
if (kDebugMode) {
binder.registerSingleton<AnalyticsService>(NoOpAnalytics());
}
},
);