Skip to content

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 configuration

1. 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

  1. Initialization Order: Core services first, then features
  2. Async Setup: Handle async initialization (SharedPreferences, etc.)
  3. Feature Coordination: Call each feature's DI configuration
  4. Singleton Services: Register shared services as singletons
  5. 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

  1. Repository Singletons: Register repositories as singletons for data consistency
  2. Cubit Factories: Register cubits as factories for multiple instances
  3. Dependency Resolution: Use di.get<Type>() to resolve dependencies
  4. Method Chaining: Use cascade operator for multiple registrations
  5. 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

  1. Type Safety: Always specify the type when getting dependencies
  2. Cubit Initialization: Call initialization methods after getting cubits
  3. Service Access: Access services directly when not using BLoC pattern
  4. 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

  1. Repositories: Always register as singletons
  2. Cubits: Always register as factories
  3. Services: Usually singletons, factories if stateful per instance
  4. Core Services: Always singletons (SharedPreferences, Logger, etc.)

Dependency Resolution

  1. Type Specification: Always specify types when getting dependencies
  2. Error Handling: Handle GetItException for missing registrations
  3. Circular Dependencies: Avoid circular dependencies between features
  4. Late Initialization: Use lazy singletons for expensive objects

Organization

  1. Feature Isolation: Each feature manages its own dependencies
  2. Global Coordination: Global DI coordinates feature registrations
  3. Clear Documentation: Document what each registration provides
  4. Consistent Patterns: Use consistent registration patterns across features

Testing

  1. DI Reset: Always reset DI container in test setup/teardown
  2. Mock Registration: Register mocks before creating objects under test
  3. Isolation: Each test should have isolated dependencies
  4. 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.