Dependency Injection
Register dependencies in modules and let the framework provide them when needed. Dependencies are scoped to their module and automatically disposed when the module is removed from the navigation tree.
Registration
Register dependencies inside the binds method of a module:
class HomeModule extends Module {
@override
FutureBinds binds(Injector i) {
i.addSingleton<HomeController>((i) => HomeController());
i.add<ApiClient>((i) => ApiClient());
}
}Dependency Types
Singleton
One instance shared across the entire module lifecycle:
i.addSingleton<DatabaseService>((i) => DatabaseService());Factory
A new instance is created every time it is requested:
i.add<ApiClient>((i) => ApiClient());Lazy Singleton
Like singleton, but only created on first use:
i.addLazySingleton<HeavyService>((i) => HeavyService());Resolving Dependencies
Using context.read
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = context.read<HomeController>();
return Scaffold(body: Text(controller.title));
}
}Using Modular.get
final controller = Modular.get<HomeController>();Using Keys
Register multiple instances of the same type by assigning unique keys:
@override
FutureBinds binds(Injector i) {
i.addSingleton<ApiService>((i) => ApiService(baseUrl: 'https://main.api'), key: 'main');
i.addSingleton<ApiService>((i) => ApiService(baseUrl: 'https://backup.api'), key: 'backup');
}
// Resolve by key
final mainApi = Modular.get<ApiService>(key: 'main');
final backupApi = context.read<ApiService>(key: 'backup');Async Dependencies
For dependencies that require async initialization:
class DatabaseModule extends Module {
@override
FutureOr<void> binds(Injector i) async {
final db = await DatabaseHelper.open();
i.addSingleton<DatabaseHelper>((i) => db);
}
}The framework shows a loading indicator automatically while async binds are being resolved. See Loader System for customization options.
Auto-Dispose
Dependencies are automatically disposed when their module leaves the navigation tree:
- User navigates to
/products—ProductsModulebinds are registered. - User navigates away —
ProductsModule.dispose()is called and all binds are cleaned up.
This prevents memory leaks without any manual cleanup.
For
StatefulShellModularRoute, branch module binds live as long as the shell is active and are disposed together when the shell exits. See Shell Routes for details.
Dependency Resolution
Dependencies can reference other registered dependencies:
class ProductsModule extends Module {
@override
FutureBinds binds(Injector i) {
i.addLazySingleton<ApiService>((i) => ApiService());
i.add<ProductsRepository>((i) => ProductsRepository(api: i.get<ApiService>()));
i.addSingleton<ProductsController>((i) => ProductsController(
repository: i.get<ProductsRepository>(),
));
}
@override
List<ModularRoute> get routes => [
ChildRoute('/', child: (_, __) => ProductsPage()),
];
}Best Practices
- Register in modules — Keep dependencies close to the feature that uses them.
- Use singletons for services — Database clients, API services, controllers.
- Use factories for short-lived objects — DTOs, form validators.
- Use keys sparingly — Only when you need multiple instances of the same type.
- Keep modules focused — One module per feature.