0022 Dependency Injection Strategy
- Author:
- Catalyst Engineering Team
- Created:
- 2024-01-15
- Status:
- Accepted
- Tags:
-
Context¶
The Catalyst Voices frontend application requires a dependency injection solution that:
- Supports constructor injection
- Enables testability by allowing mock dependencies
- Provides clear dependency registration
- Works across all Flutter platforms
- Supports both singleton and factory patterns
- Allows for lazy initialization
We evaluated several DI approaches:
- Manual constructor injection without a framework
- GetIt service locator
- Injectable (code generation)
- Riverpod (state management with DI)
- Provider (Flutter's built-in)
Decision¶
We use GetIt as a service locator with a DependencyProvider abstraction layer for manual dependency injection.
Dependencies are registered manually in a centralized Dependencies class using constructor injection.
Key aspects:
- Service Locator Pattern: GetIt provides the underlying service locator
- Abstraction Layer:
DependencyProviderabstracts GetIt for better testability - Manual Registration: All dependencies registered in
Dependenciesclass - Constructor Injection: Dependencies passed via constructors
- Registration Patterns: Support for singletons, lazy singletons, and factories
Implementation Details¶
DependencyProvider Abstraction¶
abstract class DependencyProvider {
static DependencyProvider get instance;
T get<T extends Object>();
void registerSingleton<T extends Object>(T instance);
void registerLazySingleton<T extends Object>(ValueGetter<T> factoryFunc);
void registerFactory<T extends Object>(ValueGetter<T> factoryFunc);
}
Dependencies Registration¶
final class Dependencies extends DependencyProvider {
static final Dependencies instance = Dependencies._();
Future<void> init({...}) async {
DependencyProvider.instance = this;
_registerStorages();
_registerNetwork();
_registerRepositories();
_registerServices();
_registerBlocsWithDependencies();
}
void _registerServices() {
registerLazySingleton<ProposalService>(() {
return ProposalService(
get<ProposalRepository>(),
get<DocumentRepository>(),
get<UserService>(),
// ... other dependencies
);
});
}
}
Usage in BLoCs¶
// Factory registration for BLoCs
registerFactory<ProposalsCubit>(() {
return ProposalsCubit(
get<UserService>(),
get<CampaignService>(),
get<ProposalService>(),
);
});
// Usage in widgets
final cubit = context.read<ProposalsCubit>();
Alternatives Considered¶
Injectable (Code Generation)¶
- Pros: Less boilerplate, compile-time safety
- Cons: Code generation step, less explicit, harder to debug
- Rejected: Manual registration provides better visibility and control
Riverpod¶
- Pros: Excellent DI with state management
- Cons: Different paradigm, migration cost, overkill for DI-only needs
- Rejected: BLoC already established, migration not justified
Provider¶
- Pros: Built into Flutter, simple
- Cons: Widget tree coupling, less flexible
- Rejected: Service locator pattern provides better separation
Pure Manual DI (No Framework)¶
- Pros: No dependencies, full control
- Cons: More boilerplate, manual lifecycle management
- Rejected: GetIt provides good balance of control and convenience
Consequences¶
Positive¶
- Clear dependency registration in one place
- Easy to test with mock dependencies
- Constructor injection ensures dependencies are explicit
- Service locator pattern enables static access when needed
- Abstraction layer allows swapping implementations
Negative¶
- Manual registration requires maintenance
- Service locator can hide dependencies (mitigated by constructor injection)
- GetIt dependency adds to project dependencies
Best Practices¶
- Always use constructor injection
- Register dependencies in logical groups (storages, services, repositories, BLoCs)
- Use factories for BLoCs (new instance per use)
- Use lazy singletons for services and repositories
- Use singletons for configuration and infrastructure
- Provide dispose functions for resources that need cleanup