Am I the only one who hates this sight:

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    final client = HttpClient();
    final prefs = await SharedPreferences();
    final depA = DependencyA();
    final depB = DependencyB();
    final depC = DependencyC();

    return MultiRepositoryProvider(
      providers: [
        RepositoryProvider<RepositoryA>(create: (_) => RepositoryAImpl(client, prefs)),
        RepositoryProvider<RepositoryB>(create: (_) => RepositoryBImpl(depA)),
        RepositoryProvider<RepositoryC>(create: (_) => RepositoryCImpl(depB)),
        RepositoryProvider<RepositoryD>(create: (_) => RepositoryDImpl(client)),
        RepositoryProvider<RepositoryE>(create: (_) => RepositoryEImpl(depC)),
      ],
      child: MultiBlocProvider(
        providers: [
          BlocProvider<BlocA>(
            create:
                (_) =>
                    BlocA(context.read<RepositoryA>())..add(InitEvent()),
          ),
          BlocProvider<BlocB>(
            create:
                (_) =>
                    BlocB(context.read<RepositoryB>())..add(InitEvent()),
          ),
          BlocProvider<BlocC>(
            create: (_) => BlocC(context.read<RepositoryC>()),
          ),
          BlocProvider<BlocD>(
            create:
                (_) => BlocD(
                  context.read<RepositoryD>(),
                  context.read<RepositoryE>(),
                ),
          ),
        ],
        child: MaterialApp(

// ...the rest of this miserable app

Managing dependencies efficiently is crucial, especially as application scopes grow. The get_it package can simplify dependency injection by acting as a service locator, allowing developers to register and retrieve instances easily. We’ll use this to establish all of our dependency relationships in one easy-to-read file.

Installing GetIt

Start by adding the package to your pubspec.yaml:

// pubspec.yaml

dependencies:
  get_it: ^7.6.7

Registering Dependencies

We can start by creating what I call an injection container, where we register services or repositories in a single setup function:

// injection_container.dart

final di = GetIt.I;

Future<void> setupDependencyInjection() async {
  di
    ..registerSingleton<HttpClient>(HttpClient())
    ..registerSingleton<SharedPreferences>(await SharedPreferences.getInstance())
    ..registerSingleton<RepositoryA>(RepositoryA(di()))
    ..registerFactory<BlocA>(BlocA(di())
}

Immediately, we make the GetIt singleton instance a global variable: di. We do this exclusively to invoke the dependencies we will register, later in our BlocProviders and MultiBlocProviders.

To register our dependencies, the GetIt instance provides a few methods:

registerSingleton<T>() – Creates a single instance of the class and reuses it. registerLazySingleton<T>() – Lazily creates a single instance of the class when first accessed and reuses it. registerFactory<T>() – Creates a new instance every time it’s requested. Note: Because GetIt keeps an active record of all instances registered and their type, it can infer which instance to produce when called as a constructor argument. So as long as dependency registrations are properly ordered, simply calling di() as an argument is enough.

Now let’s create the same dependency chain from the first example:

// injection_container.dart

final di = GetIt.I;

Future<void> setupDependencyInjection() async {
  di
    // Services
    ..registerSingleton<HttpClient>(HttpClient())
    ..registerSingleton<SharedPreferences>(await SharedPreferences.getInstance())
    ..registerSingleton<DependencyA>(DependencyA())
    ..registerSingleton<DependencyB>(DependencyB())
    ..registerSingleton<DependencyC>(DependencyC())

    // Repositories
    ..registerSingleton<RepositoryA>(RepositoryAImpl(di(), di()))
    ..registerSingleton<RepositoryB>(RepositoryBImpl(di()))
    ..registerSingleton<RepositoryC>(RepositoryCImpl(di()))
    ..registerSingleton<RepositoryD>(RepositoryDImpl(di()))
    ..registerSingleton<RepositoryE>(RepositoryEImpl(di()))

    // Blocs
    ..registerFactory<BlocA>(BlocA(di())
    ..registerFactory<BlocB>(BlocB(di())
    ..registerFactory<BlocC>(BlocC(di())
    ..registerFactory<BlocD>(BlocD(di(), di())
}

We then only need to call this function once before runApp():

// main.dart

void main() {
  await setupDependencyInjection();
  runApp(MyApp());
}

Accessing Dependencies

Lastly, we now only need to provide our registered Bloc instances in the MultiBlocProvider:

// app.dart

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<BlocA>(create: (_) => di())                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     ,
        BlocProvider<BlocB>(create: (_) => di()),
        BlocProvider<BlocC>(create: (_) => di()),
        BlocProvider<BlocD>(create: (_) => di()),
        BlocProvider<BlocE>(create: (_) => di()),
      ],
      child: MaterialApp(

// ...the rest of this less miserable app

…much cleaner and easier to manage.

The best part of this approach: Since we register our Blocs as “factories”, those provided to specific screens/widgets will be destroyed when their routes are popped. This avoids unused Blocs and their dependencies unnecessarily using memory.

Conclusion

The GetIt package was intended as a global service locater, but for developers who prefer explicit dependency injection, it can act as a dependency relationship manager. So whenever dependencies and their relationships change, a developer doesn’t have to scour their codebase for references.