Skip to content

πŸ’‰ Dependency Injection ​

Overview ​

Dependency Injection (DI) promotes loose coupling and testability by providing dependencies from an external container rather than having classes create them. Use GetIt as the service locator and Injectable for code generation so dependencies are registered via annotations; generated config (e.g. injection.config.dart) should not be edited by hand.

  • GetIt β€” Service locator: getIt<T>() resolves registered types.
  • Injectable β€” Scans annotated classes and generates registration code into the config file.

Define a single global locator (e.g. in lib/shared/di/app_di.dart):

dart
final getIt = GetIt.instance;

Resolve dependencies with getIt<MyType>() (or getIt.get<MyType>()).

General Principles ​

  • Inversion of Control (IoC): DI inverts object creation and dependency management to a container or factory.
  • Loose Coupling: Reduce direct dependencies between classes for modular, maintainable code.
  • Testability: Allow dependencies to be mocked or stubbed for isolated tests.
  • Reusability: Enable components to be swapped and configured without changing callers.

Initialization Flow ​

  1. Entrypoint (e.g. main.dart) calls a single app initializer (e.g. AppInitializer.init()).
  2. The initializer performs, in order:
    • WidgetsFlutterBinding.ensureInitialized()
    • Third-party init (e.g. Firebase, localization)
    • DI setup (e.g. AppDI.setup())
    • Any other bootstrap (e.g. routing, screen util)
  3. DI setup (e.g. in app_di.dart):
    • Calls configureDependencies(), which runs the generated getIt.init() and registers all injectable dependencies.
    • Any app-specific post-init (e.g. base URL, permissions) that depends on the container.

DI must be fully configured before runApp(). Use the app initializer before runApp() so getIt is ready when the app starts.

File Layout ​

FilePurpose
app_di.dartDeclares getIt, exposes setup(), and any URL/permissions/clear-user logic that uses the container
app_initializer.dartSingle init() that runs binding, third-party init, routing, DI, and other bootstrap
injection.dartDefines configureDependencies() and calls the generated getIt.init()
injection.config.dartGenerated β€” Do not edit; contains all registrations from annotations
*_module.dartInjectable @module classes for shared (e.g. core, networking) and feature-specific dependencies

Registration Guidelines ​

Injectable Annotations ​

  • @singleton β€” One instance; created when first requested (or at init if @preResolve).
  • @lazySingleton β€” One instance; created on first request.
  • @factory β€” New instance per request.
  • @preResolve β€” For async registration; the future is awaited during getIt.init() so the dependency is ready before the app runs.
  • @module β€” Class that provides dependencies (e.g. core, networking, feature modules).

Use constructor injection for types that need Dio or other registered types; GetIt will resolve them.

Lifecycle Mapping ​

NeedAnnotation / Registration
Single instance, created once@singleton or getIt.registerSingleton
Single instance, created on first use@lazySingleton or getIt.registerLazySingleton
New instance per request@factory or getIt.registerFactory
Instance with parameters@injectable + factoryParam or getIt.registerFactoryParam
  • Services / repositories: Prefer @lazySingleton (or @injectable).
  • Cubits / blocs: Prefer @injectable (factory) or @factory / factoryParam so each screen gets a new instance.
  • Feature modules: Use @module and expose services/repos as @lazySingleton; Cubits as @factory or factoryParam where needed.

Feature Modules ​

Data sources (e.g. *Services in data/data_sources/) and feature-specific services must be registered in the feature’s own module.dart, not in shared DI or another feature.

  • Path: lib/features/<feature_name>/module.dart
  • Pattern: An abstract @module class with @lazySingleton methods that take Dio (or other registered types) and return the service. Repositories that depend on these services are then registered by the code generator.

Example:

dart
@module
abstract class MyFeatureModule {
  @lazySingleton
  MyFeatureServices myFeatureServices(Dio dio) => MyFeatureServices(dio);
}

When adding a new data source or feature service, add a corresponding @lazySingleton in that feature’s module.dart, then run the build_runner so it appears in the generated config.

Code Standards for app_di.dart ​

  • app_di.dart is responsible for the locator declaration and setup() that configures the DI container.
  • setup() must call configureDependencies() (which runs the generated getIt.init()). Any app-specific setup (e.g. base URL, permissions) that uses the container can follow.
  • setup() must be invoked early in the application lifecycle, before any dependencies are resolved (via the app initializer, before runApp()).
  • Use clear, consistent names for registered types and modules.
  • Group related dependencies in modules (core, networking, features).
  • Register cubits, repos, and services needed by the app; avoid registering unused dependencies.
  • Use the single getIt instance to resolve dependencies across the app.
  • Do not edit the generated config file; regenerate it with the build_runner after annotation changes.

Adding a New Dependency ​

  1. Annotate the class in the correct layer:
    • Service/Repository: @lazySingleton (or @injectable).
    • Cubit: @injectable (defaults to factory) or @factory / factoryParam in a module when parameters are needed.
  2. Constructor injection: If the type needs Dio or other registered types, inject them via the constructor; GetIt will resolve them.
  3. Where to register:
    • Feature data sources / services: Add a @lazySingleton method in that feature’s module.dart. Do not register them in shared modules or other features.
    • Shared or cross-cutting dependencies: Use a shared @module (e.g. core, networking) in lib/shared/di/.
    • Other types can use @injectable on the class; the generator will pick them up.
  4. Run code generation: e.g. dart run build_runner build --delete-conflicting-outputs.
  5. Resolve where needed: getIt<YourType>().

Resolving Dependencies ​

  • Use getIt<Type>() for types registered as singleton or lazy singleton.
  • Use getIt<Cubit>(param1: a, param2: b) (or the generated factoryParam signature) for Cubits that take parameters; check the generated config for the exact signature.
  • Optional check: getIt.isRegistered<Type>() before use.
  • Clear flows: Use await getIt.unregister<Type>(); when tearing down (e.g. logout).

Rely on getIt and injectable-generated registrations; avoid manually registering the same type in multiple places.