Dependency Injection: Concepts & GetIt Setup
Dependency Injection: Concepts & GetIt Setup
As Flutter applications scale, classes accumulate dependencies — other objects they rely on to do their work. Without a principled approach, you end up constructing those objects deep inside business logic, making the code impossible to test and painful to swap. Dependency Injection (DI) solves this by inverting control: instead of a class creating its own dependencies, they are supplied from outside. This lesson formalises the principle and then wires a complete Clean Architecture stack using the GetIt service locator.
The Dependency Inversion Principle (DIP)
SOLID's fifth principle states:
- High-level modules (use-cases, blocs) must not depend on low-level modules (HTTP clients, databases).
- Both must depend on abstractions (abstract classes / interfaces).
- Abstractions must not depend on details — details depend on abstractions.
In practice this means your GetWeatherUseCase references a WeatherRepository interface, not the concrete WeatherRepositoryImpl that calls an API. The concrete implementation is injected at composition time.
Types of GetIt Registrations
GetIt provides three registration strategies, each with distinct object-lifetime semantics:
registerSingleton<T>(T instance)— creates the object immediately; the same instance is returned on everyget<T>()call.registerLazySingleton<T>(() => T)— defers construction until the firstget<T>(); same instance thereafter. Best for heavyweight dependencies you might not always use.registerFactory<T>(() => T)— creates a new instance on everyget<T>(). Ideal for Blocs/Cubits so each screen gets a fresh state machine.
Adding GetIt to Your Project
Add the package to pubspec.yaml, then run flutter pub get:
pubspec.yaml dependency
dependencies:
flutter:
sdk: flutter
get_it: ^7.7.0 # service locator
dio: ^5.4.0 # HTTP client (example infrastructure dep)
shared_preferences: ^2.2.3
Building the Injection Container
The convention in Clean Architecture Flutter projects is a single file — typically lib/core/di/injection_container.dart — that registers every dependency in the correct order (infrastructure first, then data, then domain, then presentation).
lib/core/di/injection_container.dart
import 'package:get_it/get_it.dart';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Domain abstractions
import '../../features/weather/domain/repositories/weather_repository.dart';
import '../../features/weather/domain/usecases/get_current_weather.dart';
// Data implementations
import '../../features/weather/data/datasources/weather_remote_datasource.dart';
import '../../features/weather/data/repositories/weather_repository_impl.dart';
// Presentation
import '../../features/weather/presentation/bloc/weather_bloc.dart';
/// Global service-locator instance — exposed for the whole app.
final GetIt sl = GetIt.instance;
/// Call once in [main] before [runApp].
Future<void> initDependencies() async {
// ── 1. External / third-party ─────────────────────────────────────
// SharedPreferences must be awaited before registration.
final sharedPrefs = await SharedPreferences.getInstance();
sl.registerSingleton<SharedPreferences>(sharedPrefs);
// Dio singleton — shared across all network calls.
sl.registerLazySingleton<Dio>(
() => Dio(BaseOptions(
baseUrl: 'https://api.openweathermap.org/data/2.5/',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
)),
);
// ── 2. Data layer ──────────────────────────────────────────────────
sl.registerLazySingleton<WeatherRemoteDataSource>(
() => WeatherRemoteDataSourceImpl(dio: sl()),
);
// Register IMPL under the ABSTRACT type so callers reference the interface.
sl.registerLazySingleton<WeatherRepository>(
() => WeatherRepositoryImpl(remoteDataSource: sl()),
);
// ── 3. Domain layer ────────────────────────────────────────────────
sl.registerLazySingleton(
() => GetCurrentWeather(repository: sl()),
);
// ── 4. Presentation layer ──────────────────────────────────────────
// Factory: every screen gets a fresh WeatherBloc instance.
sl.registerFactory(
() => WeatherBloc(getCurrentWeather: sl()),
);
}
Bootstrapping in main.dart
Await initDependencies() before calling runApp so that every singleton is ready before the widget tree mounts:
lib/main.dart
import 'package:flutter/material.dart';
import 'core/di/injection_container.dart' as di;
import 'app.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await di.initDependencies(); // register ALL services before runApp
runApp(const App());
}
Resolving Dependencies in Widgets
Call sl<T>() (or the equivalent GetIt.instance<T>()) anywhere you need an instance. The most common usage is inside a BlocProvider:
Resolving a factory-registered Bloc
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/di/injection_container.dart';
import '../bloc/weather_bloc.dart';
import 'weather_view.dart';
class WeatherPage extends StatelessWidget {
const WeatherPage({super.key});
@override
Widget build(BuildContext context) {
// sl<WeatherBloc>() calls the factory → brand-new bloc every time
return BlocProvider(
create: (_) => sl<WeatherBloc>(),
child: const WeatherView(),
);
}
}
sl<T>() inside a build() method without wrapping it in a BlocProvider or similar. Calling a factory registration directly in build() creates a new instance on every rebuild, leaking memory and losing state.Why GetIt Over Pure Constructor Injection?
Constructor injection is the purest DI form and should be preferred for domain and data layers. GetIt shines at the composition root — the single place that wires everything together. Benefits include:
- No code generation (unlike injectable or auto_route).
- Works outside the widget tree — use
sl()in plain Dart classes. - Supports async registration (
registerSingletonAsync) for awaitable initialisations. - Easy to reset in tests:
sl.reset()wipes all registrations for a clean slate.
Summary
Dependency Injection, guided by the Dependency Inversion Principle, decouples your layers and makes each component independently testable. GetIt's three registration modes — singleton, lazy singleton, and factory — map cleanly onto Clean Architecture lifetimes: infrastructure lives as long as the app, while Blocs are recreated per screen. A single injection_container.dart file, initialised in main(), is all you need to wire the entire dependency graph.