FlutterDependency InjectionGet_itApr 29, 20265 min read313

Leveling Up Your Flutter Architecture: Mastering Dependency Injection with get_it and injectable

If you’ve been building Flutter apps for a while, you’ve likely run into the “spaghetti code” phase. It happens when your UI widgets are tightly coupled to your business logic, and your business logic is directly instantiating API clients and databases.

When you try to write a unit test or swap out a database, everything breaks.

The solution? Dependency Inversion* and *Dependency Injection (DI). In the Flutter ecosystem, the golden duo for handling this is get_it combined with injectable. Let’s break down what these concepts mean and how to implement them to write cleaner, scalable Flutter apps.

1_eiijD7qUUP24wWPr8LnMqQ.webp

Table of Contents

  • 🧠 The Theory: Dependency Inversion vs. Injection
  • - 1. Dependency Inversion Principle (DIP) - 2. Dependency Injection (DI)
  • 🛠 Enter the Heroes: get_it & injectable
  • 🚀 Step-by-Step Implementation
  • - Step 1: Add Dependencies - Step 2: Create the Abstraction - Step 3: Create the Concrete Implementation - Step 4: Setup the Injection Configuration - Step 5: Run the Build Runner - Step 6: Initialize and Use!
  • 🎯 Why is this approach so powerful?
  • Wrapping Up
  • ---

    🧠 The Theory: Dependency Inversion vs. Injection

    Before diving into the code, let’s clear up the terminology. They sound similar, but they play different roles in the SOLID principles.

    1. Dependency Inversion Principle (DIP)

    The “D” in SOLID. It states two things:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).

  • Abstractions should not depend on details. Details should depend on abstractions.
  • In plain English: Don’t let your UI directly depend on a FirebaseAuthService. Instead, let your UI depend on an abstract AuthService. This way, if you switch from Firebase to Supabase tomorrow, your UI doesn't need to change.

    2. Dependency Injection (DI)

    DI is the technique used to achieve the Dependency Inversion Principle. Instead of a class creating its own dependencies (e.g., final api = ApiClient();), the dependency is "injected" into the class from the outside, usually via the constructor.

    ---

    🛠 Enter the Heroes: get_it & injectable

    While you can pass dependencies down the widget tree manually, it gets exhausting fast. This is where our Flutter packages come in.

  • get_it: A highly efficient Service Locator. It acts as a central registry where you can register your classes and request them from anywhere in your app without context.
  • injectable: A code generator for get_it. Manually registering dozens of dependencies in get_it creates a massive, messy setup file. injectable allows you to use simple annotations (@injectable, @singleton) on your classes, and it writes the get_it boilerplate for you!
  • ---

    🚀 Step-by-Step Implementation

    Let’s build a simple example: An app that fetches weather data. We want our UI to rely on an abstraction (WeatherRepository), and we want injectable to wire up the concrete implementation (WeatherRepositoryImpl).

    Step 1: Add Dependencies

    Add the following to your pubspec.yaml:

    dependencies:
      get_it: ^7.6.4
      injectable: ^2.3.2
    
    dev_dependencies:
      injectable_generator: ^2.4.1
      build_runner: ^2.4.6
    
    Always check pub.dev for the latest versions.

    Step 2: Create the Abstraction

    Create an abstract class for your repository. This is what your high-level code (like Blocs, Providers, or ViewModels) will talk to.

    // weather_repository.dart
    abstract class WeatherRepository {
      Future<String> getCurrentWeather(String city);
    }
    

    Step 3: Create the Concrete Implementation

    Now, create the actual implementation. Here is where the magic of injectable happens. We use the @LazySingleton annotation and tell it to bind to the abstract WeatherRepository.

    // weather_repository_impl.dart
    import 'package:injectable/injectable.dart';
    import 'weather_repository.dart';
    
    @LazySingleton(as: WeatherRepository)
    class WeatherRepositoryImpl implements WeatherRepository {
      
      // If this class had dependencies (like a network client), 
      // you would inject them via the constructor here!
      
      @override
      Future<String> getCurrentWeather(String city) async {
        // Imagine an API call here
        await Future.delayed(const Duration(seconds: 1));
        return "Sunny and 25°C in $city";
      }
    }
    

    Step 4: Setup the Injection Configuration

    Create a file named injection.dart. This is the entry point for get_it and injectable.

    // injection.dart
    import 'package:get_it/get_it.dart';
    import 'package:injectable/injectable.dart';
    import 'injection.config.dart'; // This file will be generated
    
    final getIt = GetIt.instance;
    
    @InjectableInit(
      initializerName: 'init', // default
      preferRelativeImports: true, // default
      asExtension: true, // default
    )
    void configureDependencies() => getIt.init();
    

    Step 5: Run the Build Runner

    Now, tell Flutter to generate the wiring code. Run this command in your terminal:

    dart run build_runner build --delete-conflicting-outputs
    
    Use watch instead of build if you want it to auto-generate every time you save a file.

    Step 6: Initialize and Use!

    Finally, initialize your dependencies in main.dart before running the app.

    // main.dart
    import 'package:flutter/material.dart';
    import 'injection.dart';
    
    void main() {
      // Initialize dependencies
      configureDependencies(); 
      runApp(const MyApp());
    }
    

    Now, anywhere in your app (like inside a Cubit, a Bloc, or even directly in a widget), you can retrieve your dependency like this:

    // Fetching the dependency via the abstraction!
    final weatherRepo = getIt<WeatherRepository>();
    
    // Use it
    final weather = await weatherRepo.getCurrentWeather('Tokyo');
    

    ---

    🎯 Why is this approach so powerful?

  • Zero Boilerplate: injectable handles all the getIt.registerLazySingleton<WeatherRepository>(() => WeatherRepositoryImpl()); code for you.
  • Testability: In your unit tests, you can easily mock WeatherRepository and register the mock in get_it. Your business logic won't know the difference.
  • Clean Architecture: Your UI and State Management layers are completely decoupled from your network and database layers.
  • ---

    Wrapping Up

    Mastering get_it and injectable might take an hour or two to fully wrap your head around, but it pays dividends for the lifetime of your project. It forces you to think in abstractions, making your codebase modular, testable, and highly professional.

    Happy coding! 🚀

    Enjoyed this read?

    3likes
    Comments

    Loading…