Flutter Dependency Injection with get_it

A class rarely works alone. A ViewModel needs a repository, the repository needs an API client, and the API client needs a configuration object. Creating each helper inside the class that uses it locks everything together. Dependency injection solves this by handing every class the helpers it needs from outside. The get_it package acts as the central storage room where those helpers live.

The Problem with Building Your Own Helpers


class ProductViewModel {
  final repository = ProductRepositoryImpl(ProductApi(Dio()));
}

This class creates its own repository, API, and Dio client. Testing it requires a real network. Swapping the API means editing the ViewModel. Ten screens that build their own copies waste memory and risk inconsistent settings.

The Solution: Ask from Outside

Think of a kitchen. A chef does not grow vegetables and forge knives before cooking. A supplier delivers ingredients, and the chef focuses on the dish. Dependency injection makes your classes chefs, and get_it plays the supplier.


  main() ── register ──→ ┌──────────────────┐
                         │  get_it storage  │
                         │  Dio             │
                         │  ProductApi      │
                         │  ProductRepo     │
                         └────────┬─────────┘
                                  │ getIt<ProductRepo>()
                                  ▼
                            ProductViewModel

Installing


flutter pub add get_it

Creating the Locator

Create one global instance, and register every service at app start.


import 'package:get_it/get_it.dart';

final GetIt getIt = GetIt.instance;

void setupLocator() {
  getIt.registerLazySingleton<Dio>(() => Dio(BaseOptions(baseUrl: 'https://api.example.com')));

  getIt.registerLazySingleton<ProductApi>(() => ProductApi(getIt<Dio>()));

  getIt.registerLazySingleton<ProductRepository>(
    () => ProductRepositoryImpl(getIt<ProductApi>(), ProductCache()),
  );

  getIt.registerFactory<ProductViewModel>(
    () => ProductViewModel(getIt<ProductRepository>()),
  );
}

void main() {
  setupLocator();
  runApp(MyApp());
}

Registration Types

MethodBehaviorUse For
registerSingletonCreates the object immediately and shares itServices needed at startup
registerLazySingletonCreates the object on first request and shares itRepositories, API clients
registerFactoryCreates a new object on every requestViewModels, per-screen state

  Singleton:  getIt() ─→ same object ─→ same object ─→ same object
  Factory:    getIt() ─→ new object ─→ new object ─→ new object

Using Registered Objects


class _ProductScreenState extends State<ProductScreen> {
  final ProductViewModel viewModel = getIt<ProductViewModel>();

  @override
  void initState() {
    super.initState();
    viewModel.load();
  }
}

The screen never mentions the repository, the API, or Dio. It asks for a ViewModel and receives a fully assembled one.

Swapping Implementations for Tests

A test registers fakes instead of real services.


setUp(() {
  getIt.reset();
  getIt.registerLazySingleton<ProductRepository>(() => FakeProductRepository());
  getIt.registerFactory<ProductViewModel>(
    () => ProductViewModel(getIt<ProductRepository>()),
  );
});

The same trick switches between a staging server and a production server through environment settings.

Constructor Injection Versus Service Locator

Two ideas often blend together. Constructor injection means a class receives its helpers as constructor parameters. A service locator means a class asks a global registry for its helpers. The code above combines both: get_it assembles objects, and constructors receive them. Prefer constructor parameters inside business classes. Call getIt only in the outer layer, such as screens and the setup function.

Other Options

ToolIdea
get_itGlobal registry, no widget tree needed
ProviderInjects through the widget tree
RiverpodProviders act as injected, testable values

Pick one approach per project. Mixing all three confuses teammates.

Practice Ideas

Register a repository and a ViewModel, then load data in a screen through getIt. Write a test that registers a fake repository and checks the ViewModel's list. Print a message in each constructor to watch how singletons and factories differ.

A Mental Model for Lifetimes

Every object lives for a certain length of time. An API client lives as long as the app runs, so a singleton fits. A screen's ViewModel lives only while that screen is open, so a factory fits. Mixing these up causes subtle bugs: a singleton ViewModel remembers old search text when the user returns, and a factory API client throws away its cached login token. Ask one question for each registration: how long should this object live? The answer picks the method.

Scopes for Temporary Services

Some objects belong to a logged-in session. get_it supports scopes through pushNewScope and popScope. Register user-specific services when the person signs in, and drop the whole scope when they sign out. This approach guarantees that no data from one account leaks into the next.