Dependency Injection Guide
This project uses GetIt as a service locator for dependency injection. Each feature manages its own dependencies while sharing common services through the global DI container.
Structure
feature/
├── di.dart # Feature-specific DI configuration
├── domain/
├── data/
└── presentation/app/
└── di.dart # Global DI configuration1. Global Dependency Injection
The global DI container (lib/app/di.dart) manages app-wide services and coordinates feature-specific DI setup.
Global DI Template
dart
import 'package:flutter_boilerplate/core/services/logger_service.dart';
import 'package:flutter_boilerplate/features/auth/di.dart';
import 'package:flutter_boilerplate/features/[feature]/di.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Global dependency injection container.
///
/// Manages app-wide services and coordinates feature-specific
/// dependency registration.
final GetIt di = GetIt.instance;
/// Configures all application dependencies.
///
/// Should be called during app initialization before runApp().
/// Registers core services first, then feature-specific dependencies.
Future<void> configureDependencies() async {
// Register core services
await _configureCoreServices();
// Register feature dependencies
_configureFeatureDependencies();
}
/// Configures core application services.
///
/// These services are used across multiple features and should be
/// registered as singletons for consistency.
Future<void> _configureCoreServices() async {
// SharedPreferences - singleton for data persistence
final sharedPreferences = await SharedPreferences.getInstance();
di.registerSingleton<SharedPreferences>(sharedPreferences);
// Logger service - singleton for consistent logging
di.registerSingleton<LoggerService>(LoggerService());
// Add other core services here
// di.registerSingleton<AnalyticsService>(AnalyticsService());
// di.registerSingleton<CrashReportingService>(CrashReportingService());
}
/// Configures feature-specific dependencies.
///
/// Calls each feature's DI configuration function to register
/// feature-specific repositories, cubits, and services.
void _configureFeatureDependencies() {
configureAuthDependencies();
configureFeatureDependencies();
// Add other feature configurations here
}Key Points for Global DI
- Initialization Order: Core services first, then features
- Async Setup: Handle async initialization (SharedPreferences, etc.)
- Feature Coordination: Call each feature's DI configuration
- Singleton Services: Register shared services as singletons
- Documentation: Document the purpose and initialization order
2. Feature Dependency Injection
Each feature has its own DI configuration that registers repositories, cubits, and feature-specific services.
Feature DI Template
dart
import 'package:flutter_boilerplate/app/di.dart';
import 'package:flutter_boilerplate/features/[feature]/data/repositories/local_feature_repository.dart';
import 'package:flutter_boilerplate/features/[feature]/domain/repositories/feature_repository.dart';
import 'package:flutter_boilerplate/features/[feature]/presentation/cubit/feature_cubit.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Configures the [FeatureName] feature dependencies.
///
/// Registers [LocalFeatureRepository] as a singleton for data persistence
/// and [FeatureCubit] as a factory for state management.
void configureFeatureDependencies() {
// Register repository as singleton
// Repositories should be singletons to maintain data consistency
di.registerSingleton<FeatureRepository>(
LocalFeatureRepository(
sharedPreferences: di.get<SharedPreferences>(),
),
);
// Register cubit as factory
// Cubits should be factories to allow multiple instances per page
di.registerFactory<FeatureCubit>(
() => FeatureCubit(
di.get<FeatureRepository>(),
),
);
// Register additional feature services if needed
// di.registerSingleton<FeatureService>(
// FeatureService(
// repository: di.get<FeatureRepository>(),
// ),
// );
}Real Example (Notes Feature)
dart
import 'package:flutter_boilerplate/app/di.dart';
import 'package:flutter_boilerplate/features/notes/data/repositories/local_notes_repository.dart';
import 'package:flutter_boilerplate/features/notes/domain/repositories/notes_repository.dart';
import 'package:flutter_boilerplate/features/notes/presentation/cubit/notes_cubit.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Configures the notes feature dependencies.
///
/// Registers [LocalNotesRepository] as a singleton for notes persistence
/// and [NotesCubit] as a factory for state management.
void configureNotesDependencies() {
di
..registerSingleton<NotesRepository>(
LocalNotesRepository(
sharedPreferences: di.get<SharedPreferences>(),
),
)
..registerFactory<NotesCubit>(
() => NotesCubit(
di.get<NotesRepository>(),
),
);
}Key Points for Feature DI
- Repository Singletons: Register repositories as singletons for data consistency
- Cubit Factories: Register cubits as factories for multiple instances
- Dependency Resolution: Use
di.get<Type>()to resolve dependencies - Method Chaining: Use cascade operator for multiple registrations
- Documentation: Document what each registration provides
3. Registration Patterns
Singleton Registration
Use for services that should have only one instance:
dart
// ✅ Good for repositories, services, and shared state
di.registerSingleton<FeatureRepository>(
LocalFeatureRepository(
sharedPreferences: di.get<SharedPreferences>(),
),
);
// ✅ Good for core services
di.registerSingleton<LoggerService>(LoggerService());Factory Registration
Use for objects that need multiple instances:
dart
// ✅ Good for cubits, controllers, and page-specific objects
di.registerFactory<FeatureCubit>(
() => FeatureCubit(
di.get<FeatureRepository>(),
),
);
// ✅ Good for services that maintain per-instance state
di.registerFactory<ValidationService>(
() => ValidationService(),
);Lazy Singleton Registration
Use for expensive objects that might not be needed immediately:
dart
// ✅ Good for heavy services or those with async initialization
di.registerLazySingleton<DatabaseService>(
() => DatabaseService(),
);4. Using Dependencies in Widgets
BlocProvider with DI
dart
class FeaturePage extends StatelessWidget {
const FeaturePage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
// ✅ Get cubit from DI container
create: (context) => di<FeatureCubit>()..listenForChanges(),
child: const _FeaturePageBody(),
);
}
}Direct Service Access
dart
class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ✅ Access services directly when needed
final logger = di<LoggerService>();
logger.d('Widget built');
return Container();
}
}Key Points for Usage
- Type Safety: Always specify the type when getting dependencies
- Cubit Initialization: Call initialization methods after getting cubits
- Service Access: Access services directly when not using BLoC pattern
- Error Handling: Handle potential registration errors
5. Environment-Specific Dependencies
Development vs Production
dart
void configureFeatureDependencies() {
if (kDebugMode) {
// ✅ Use debug/mock implementations in development
di.registerSingleton<FeatureRepository>(
DebugFeatureRepository(),
);
} else {
// ✅ Use production implementations
di.registerSingleton<FeatureRepository>(
ApiFeatureRepository(
dio: di.get<Dio>(),
),
);
}
// Cubit registration remains the same
di.registerFactory<FeatureCubit>(
() => FeatureCubit(
di.get<FeatureRepository>(),
),
);
}Feature Flags
dart
void configureFeatureDependencies() {
// ✅ Use feature flags to switch implementations
if (FeatureFlags.useLocalStorage) {
di.registerSingleton<FeatureRepository>(
LocalFeatureRepository(
sharedPreferences: di.get<SharedPreferences>(),
),
);
} else {
di.registerSingleton<FeatureRepository>(
ApiFeatureRepository(
dio: di.get<Dio>(),
),
);
}
}6. Testing with DI
Test Setup
dart
void main() {
group('FeatureCubit', () {
late MockFeatureRepository mockRepository;
late FeatureCubit cubit;
setUp(() {
// ✅ Reset DI container for each test
di.reset();
// ✅ Register mock dependencies
mockRepository = MockFeatureRepository();
di.registerSingleton<FeatureRepository>(mockRepository);
// ✅ Create cubit with mocked dependencies
cubit = FeatureCubit(di.get<FeatureRepository>());
});
tearDown(() {
// ✅ Clean up after each test
di.reset();
});
test('should perform operation', () async {
// Test implementation
});
});
}Widget Testing
dart
void main() {
group('FeaturePage', () {
late MockFeatureCubit mockCubit;
setUp(() {
di.reset();
mockCubit = MockFeatureCubit();
di.registerFactory<FeatureCubit>(() => mockCubit);
});
tearDown(() {
di.reset();
});
testWidgets('should display items', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: FeaturePage(),
),
);
// Test assertions
});
});
}7. Best Practices
Registration Guidelines
- Repositories: Always register as singletons
- Cubits: Always register as factories
- Services: Usually singletons, factories if stateful per instance
- Core Services: Always singletons (SharedPreferences, Logger, etc.)
Dependency Resolution
- Type Specification: Always specify types when getting dependencies
- Error Handling: Handle
GetItExceptionfor missing registrations - Circular Dependencies: Avoid circular dependencies between features
- Late Initialization: Use lazy singletons for expensive objects
Organization
- Feature Isolation: Each feature manages its own dependencies
- Global Coordination: Global DI coordinates feature registrations
- Clear Documentation: Document what each registration provides
- Consistent Patterns: Use consistent registration patterns across features
Testing
- DI Reset: Always reset DI container in test setup/teardown
- Mock Registration: Register mocks before creating objects under test
- Isolation: Each test should have isolated dependencies
- Cleanup: Clean up DI state after tests
The dependency injection system provides a clean way to manage dependencies while maintaining testability and flexibility. Keep registrations focused and well-documented for maintainability.