Dependency Injection

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:

  1. User navigates to /productsProductsModule binds are registered.
  2. 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

  1. Register in modules — Keep dependencies close to the feature that uses them.
  2. Use singletons for services — Database clients, API services, controllers.
  3. Use factories for short-lived objects — DTOs, form validators.
  4. Use keys sparingly — Only when you need multiple instances of the same type.
  5. Keep modules focused — One module per feature.