Appearance
π 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 β
- Entrypoint (e.g.
main.dart) calls a single app initializer (e.g.AppInitializer.init()). - 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)
- DI setup (e.g. in
app_di.dart):- Calls
configureDependencies(), which runs the generatedgetIt.init()and registers all injectable dependencies. - Any app-specific post-init (e.g. base URL, permissions) that depends on the container.
- Calls
DI must be fully configured before runApp(). Use the app initializer before runApp() so getIt is ready when the app starts.
File Layout β
| File | Purpose |
|---|---|
app_di.dart | Declares getIt, exposes setup(), and any URL/permissions/clear-user logic that uses the container |
app_initializer.dart | Single init() that runs binding, third-party init, routing, DI, and other bootstrap |
injection.dart | Defines configureDependencies() and calls the generated getIt.init() |
injection.config.dart | Generated β Do not edit; contains all registrations from annotations |
*_module.dart | Injectable @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 duringgetIt.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 β
| Need | Annotation / 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/factoryParamso each screen gets a new instance. - Feature modules: Use
@moduleand expose services/repos as@lazySingleton; Cubits as@factoryorfactoryParamwhere 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
@moduleclass with@lazySingletonmethods that takeDio(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.dartis responsible for the locator declaration andsetup()that configures the DI container.setup()must callconfigureDependencies()(which runs the generatedgetIt.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, beforerunApp()).- 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
getItinstance 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 β
- Annotate the class in the correct layer:
- Service/Repository:
@lazySingleton(or@injectable). - Cubit:
@injectable(defaults to factory) or@factory/factoryParamin a module when parameters are needed.
- Service/Repository:
- Constructor injection: If the type needs
Dioor other registered types, inject them via the constructor; GetIt will resolve them. - Where to register:
- Feature data sources / services: Add a
@lazySingletonmethod in that featureβsmodule.dart. Do not register them in shared modules or other features. - Shared or cross-cutting dependencies: Use a shared
@module(e.g. core, networking) inlib/shared/di/. - Other types can use
@injectableon the class; the generator will pick them up.
- Feature data sources / services: Add a
- Run code generation: e.g.
dart run build_runner build --delete-conflicting-outputs. - 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 generatedfactoryParamsignature) 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.