Dependency Injection and Service Locator

Dependency injection is a design pattern that allows a class to receive its dependencies from external sources rather than creating them internally. This decouples the class from its dependencies, making the code easier to test, maintain, and evolve. The most common way to inject dependencies is through constructor injection, where the dependencies are passed to the class's constructor when the object is created.

Service locator, on the other hand, is a design pattern that allows a class to look up dependencies in a central registry, either as a class factory or a singleton.

In this part of the tutorial, we're going to cover the following:

  • Combine both design patterns by using a service locator to manage the creation and storage of class factories and singletons, as well as to inject dependencies into these classes.

  • Setup the app entry point to initialize the service locator and dependencies.

1. Create the service locator instance

The service locator is primarily used by the Presentation Layer to call Use Cases. So, in the note_app/modules/presentation/lib/presentation.dart barrel file, let's set a serviceLocator variable to the default GetIt.instance:

import 'package:get_it/get_it.dart';

final serviceLocator = GetIt.instance;

Once done, fetch the dependencies so that the main module can access the serviceLocator:

flutter pub get modules/presentation

2. Create the service locator initializer

In the note_app/lib/ directory, create a file named injectables.dart and create a function called initializeServiceLocator. This will be used to inject the external dependencies, data sources, repositories and use cases using the serviceLocator previously created.

In this example, all external dependencies, Data Sources and Repositories will be registered as a Lazy Singleton, whereas all Use Cases will be registered as a Class Factory.

/// Initializes the service locator by registering all external dependencies, 
/// data sources, repositories and use cases.
void initializeServiceLocator() {
  serviceLocator.registerLazySingleton<ExceptionConverterSuite>(
    () => ExceptionConverterSuite(),
  );

  serviceLocator
      .registerLazySingleton<SqlbriteDataSource>(() => SqlbriteDataSource());

  // When injecting repositories, make sure to use the interface as the generic
  // type (i.e. NoteRepository instead of NoteRepositoryImpl) so that the
  // repository can be swapped out with a different implementation
  serviceLocator.registerLazySingleton<NoteRepository>(
    () => NoteRepositoryImpl(
      exceptionConverterSuite: serviceLocator(),
      sqlbriteDataSource: serviceLocator(),
    ),
  );

  // Inject use cases as factories so that they can be instantiated more than
  // once

  serviceLocator.registerFactory<CreateNoteEntry>(
    () => CreateNoteEntry(noteRepository: serviceLocator()),
  );

  serviceLocator.registerFactory<UpdateNoteEntry>(
    () => UpdateNoteEntry(noteRepository: serviceLocator()),
  );

  serviceLocator.registerFactory<DeleteNoteEntry>(
    () => DeleteNoteEntry(noteRepository: serviceLocator()),
  );

  serviceLocator.registerFactory<FetchNoteEntries>(
    () => FetchNoteEntries(noteRepository: serviceLocator()),
  );

  serviceLocator.registerFactory<WatchNoteEntries>(
    () => WatchNoteEntries(noteRepository: serviceLocator()),
  );
}

3. Initialize the service locator and dependencies

In the note_app/lib/main.dart file, initialize the service locator and all dependencies that require initialization:

The MainApp widget will be created at a later part of the tutorial.

import 'package:flutter/material.dart';
import 'package:infrastructure/data_sources/sqlbrite/sqlbrite_data_source.dart';
import 'package:note_app/injectables.dart';
import 'package:presentation/main_app.dart';
import 'package:presentation/presentation.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  initializeServiceLocator();

  // Initialize the [SqlbriteDataSource] before `runApp` is called to setup
  // the database
  await serviceLocator<SqlbriteDataSource>().initialize();

  // TODO: This will be created later
  runApp(const MainApp());
}

Last updated