๐ Injectable Integration โ
The modularity_injectable package replaces manual binds()/exports() wiring with injectable code generation while preserving module boundaries.
When to Use โ
When to Use Injectable
| Approach | Best for |
|---|---|
Manual binds()/exports() | Small modules (< 5 registrations), no build_runner needed |
modularity_injectable | 10+ dependencies, constructor auto-injection, teams already using injectable/get_it |
Both approaches coexist in the same app.
Setup โ
1. Add dependencies โ
dependencies:
modularity_core: ^0.2.0
modularity_injectable: ^0.2.0
get_it: ^8.0.0
injectable: ^2.3.0
dev_dependencies:
build_runner: ^2.4.0
injectable_generator: ^2.4.02. Configure the binder factory โ
Injectable requires GetItBinder (dual-scope variant from modularity_injectable).
Flutter:
import 'package:modularity_flutter/modularity_flutter.dart';
import 'package:modularity_injectable/modularity_injectable.dart';
ModularityRoot(
binderFactory: const GetItBinderFactory(),
root: RootModule(),
child: const MyApp(),
);Pure Dart:
import 'package:modularity_injectable/modularity_injectable.dart';
final controller = ModuleController(
RootModule(),
binder: GetItBinder(),
binderFactory: const GetItBinderFactory(),
);
await controller.initialize({});Annotating Dependencies โ
Annotation Guidance
- Use
@LazySingleton()for stateful services, repositories, and caches -- one instance per module scope. - Use
@Injectable()for stateless use cases and transient objects -- new instance on every resolve. - Use
@LazySingleton(as: AbstractType)to register an implementation against its interface.
Private (module-internal) โ
Standard injectable annotations. No export marker means the dependency stays private:
@LazySingleton(as: AuthRepository)
class AuthRepositoryImpl implements AuthRepository {
AuthRepositoryImpl(this._apiClient);
final ApiClient _apiClient;
}
@Injectable()
class LoginUseCase {
LoginUseCase(this._repo);
final AuthRepository _repo;
}Exported (visible to importing modules) โ
Add the modularity_export environment. Two equivalent syntaxes:
// Option A: env parameter
@LazySingleton(env: [modularityExportEnvName])
class AuthService {
AuthService(this._repo);
final AuthRepository _repo;
}
// Option B: separate annotation
@modularityExportEnv
@LazySingleton()
class AuthFacade {
AuthFacade(this._service);
final AuthService _service;
}modularityExportEnvName is the string 'modularity_export'. modularityExportEnv is const Environment('modularity_export').
Wiring the Module โ
1. Create the injectable config file โ
// lib/modules/auth/auth_injectable.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'auth_injectable.config.dart';
@InjectableInit(initializerName: 'initAuthDeps', asExtension: false)
GetIt initAuthDeps(
GetIt getIt, {
String? environment,
EnvironmentFilter? environmentFilter,
}) =>
$initGetIt(
getIt,
environment: environment,
environmentFilter: environmentFilter,
);Run code generation:
dart run build_runner build --delete-conflicting-outputs2. Wire into the Module โ
import 'package:modularity_core/modularity_core.dart';
import 'package:modularity_injectable/modularity_injectable.dart';
import 'auth_injectable.dart';
class AuthModule extends Module {
@override
void binds(Binder i) {
ModularityInjectableBridge.configureInternal(i, initAuthDeps);
}
@override
void exports(Binder i) {
ModularityInjectableBridge.configureExports(i, initAuthDeps);
}
@override
List<Type> get expects => [ApiClient];
}configureInternalregisters all annotated dependencies into the private scope.configureExportsregisters only dependencies tagged withmodularity_exportinto the public scope via aModularityExportOnlyenvironment filter.
Registration Flow โ
Mixing manual and generated registrations โ
class AuthModule extends Module {
@override
void binds(Binder i) {
ModularityInjectableBridge.configureInternal(i, initAuthDeps);
i.registerSingleton<AuthConfig>(AuthConfig.fromEnv());
}
@override
void exports(Binder i) {
ModularityInjectableBridge.configureExports(i, initAuthDeps);
}
}How It Works โ
Dual-scope GetItBinder โ
Each GetItBinder manages two isolated GetIt containers:
- Private scope (
internalContainer) -- receivesbinds()registrations - Public scope (
publicContainer) -- receivesexports()registrations
Dependency resolution order:
- Local private scope
- Local public scope
- Imports (public exports from imported modules)
- Parent module's binder
BinderGetIt -- the GetIt proxy โ
BinderGetIt is a proxy, not a real GetIt
Injectable-generated code calls getIt.get<T>() for constructor parameters. BinderGetIt wraps a GetIt instance and intercepts get<T>() to bridge modularity's scoping with GetIt's flat registry. It does not extend GetIt -- it implements its interface and delegates selectively.
Resolution steps inside BinderGetIt.get<T>():
- Named/parameterized lookups delegate directly to GetIt
- If
Tis registered locally, return it - Otherwise, fall back to
Binder.tryGet<T>()(walks imports + parent) - If still unresolved, throw native GetIt error
This lets injectable factories depend on types from imported modules automatically:
// ApiClient is exported by NetworkModule (an import).
// Injectable resolves it through BinderGetIt -- no manual i.get() needed.
@LazySingleton()
class AuthRepositoryImpl implements AuthRepository {
AuthRepositoryImpl(this._apiClient);
final ApiClient _apiClient;
}Error handling โ
Passing a non-GetIt binder (e.g. SimpleBinder) to the bridge throws ModuleConfigurationException immediately:
ModuleConfigurationException: Injectable integration requires GetItBinder.
Provide GetItBinderFactory to ModularityRoot or ModuleController.WARNING
Make sure to set GetItBinderFactory on ModularityRoot or ModuleController before using injectable integration. The bridge validates the binder type at call time.