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
| Method | Behavior | Use For |
|---|---|---|
| registerSingleton | Creates the object immediately and shares it | Services needed at startup |
| registerLazySingleton | Creates the object on first request and shares it | Repositories, API clients |
| registerFactory | Creates a new object on every request | ViewModels, 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
| Tool | Idea |
|---|---|
| get_it | Global registry, no widget tree needed |
| Provider | Injects through the widget tree |
| Riverpod | Providers 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.
