Dependency Injection
Managing dependencies in Flutter using GetIt โ a simple, type-safe service locator.
Dependency Injection (DI) is a design pattern where objects receive their dependencies from external sources rather than creating them internally. This makes your code more testable , maintainable , and flexible .
๐ก Key Concept
Instead of a class creating its own dependencies (tight coupling), dependencies are injected from the outside. This follows the Dependency Inversion Principle โ high-level modules should not depend on low-level modules, both should depend on abstractions.
โ Without DI (Tight Coupling)
- Class creates its own dependencies
- Hard to test (can't mock)
- Difficult to change implementations
- Violates Single Responsibility
- Hidden dependencies
โ With DI (Loose Coupling)
- Dependencies are passed from outside
- Easy to test (mock dependencies)
- Swap implementations easily
- Clear, explicit dependencies
- Follows SOLID principles
class
WeatherService
{
// Creates its own dependency - tight coupling!
final
ApiClient apiClient =
ApiClient
();
Future
<Weather>
getWeather
() async {
return
await
apiClient.
fetchWeather
();
}
}
class
WeatherService
{
// Dependency is injected - loose coupling!
final
ApiClient apiClient;
WeatherService
(
this
.apiClient);
Future
<Weather>
getWeather
() async {
return
await
apiClient.
fetchWeather
();
}
}
While Dependency Injection and Service Locator are often used together, they serve different purposes:
Dependency Injection
- Dependencies are passed via constructor
- Dependencies are explicit and visible
- Better for testability
- Follows Inversion of Control
- Dependencies are known at compile time
Service Locator (GetIt)
- Dependencies are requested from a central registry
- Dependencies can be resolved at runtime
- Convenient for Flutter apps
- No BuildContext required
- O(1) lookups
โ The Flutter Approach
In Flutter, we often use GetIt (service locator) together with constructor injection . Services are registered with GetIt and then injected into classes that need them. This gives us the best of both worlds.
GetIt is a simple, type-safe service locator for Dart and Flutter that makes dependency management simple. It gives you O(1) access to your objects from anywhere in your app โ no BuildContext required, no code generation.
dependencies:
flutter:
sdk: flutter
get_it: ^8.0.2
GetIt provides three main registration types to control the lifetime of your dependencies:
import
'package:get_it/get_it.dart'
;
import
'../data/datasources/weather_remote_datasource.dart'
;
import
'../data/datasources/weather_local_datasource.dart'
;
import
'../data/repositories_impl/weather_repository_impl.dart'
;
import
'../domain/repositories/weather_repository.dart'
;
import
'../domain/usecases/get_weather_usecase.dart'
;
final
getIt = GetIt.instance;
void
setupServiceLocator
() {
// ========== SINGLETONS ==========
// Create once, share everywhere
getIt.
registerSingleton
<WeatherRemoteDataSource>(
WeatherRemoteDataSource(
client: http.Client(),
baseUrl:
'https://api.openweathermap.org/data/2.5'
,
apiKey:
'your-api-key'
,
),
);
getIt.
registerSingleton
<WeatherLocalDataSource>(
WeatherLocalDataSource(),
);
// ========== LAZY SINGLETONS ==========
// Created only when first accessed
getIt.
registerLazySingleton
<WeatherRepository>(
()
{
return
WeatherRepositoryImpl(
remoteDataSource: getIt<WeatherRemoteDataSource>(),
localDataSource: getIt<WeatherLocalDataSource>(),
);
});
// ========== FACTORIES ==========
// New instance each time
getIt.
registerFactory
<GetWeatherUseCase>(
()
{
return
GetWeatherUseCase(getIt<WeatherRepository>());
});
// ========== ASYNC INITIALIZATION ==========
// For dependencies that need async setup
getIt.
registerSingletonAsync
<Database>(
()
async {
final
db =
await
Database
.open();
return
db;
});
}
๐ก Which Registration Type to Use?
- Singleton โ Services that maintain state (e.g., ApiClient, Database)
- Lazy Singleton โ Services that are expensive to create and may not always be used
- Factory โ Stateless services or objects with short lifetimes
- Async โ Dependencies that require async initialization (e.g., opening a database)
Once registered, dependencies can be accessed from anywhere in your app:
import
'package:get_it/get_it.dart'
;
// Simple access
final
repository = GetIt.instance<WeatherRepository>();
// Or using the global getIt instance
final
useCase = getIt<GetWeatherUseCase>();
// Access in a ViewModel
class
WeatherViewModel
extends
StateNotifier<WeatherState> {
final
GetWeatherUseCase getWeatherUseCase;
WeatherViewModel
()
: getWeatherUseCase = getIt<GetWeatherUseCase>(),
super
(WeatherState.
initial
());
// Or with constructor injection (recommended)
// WeatherViewModel(this.getWeatherUseCase) : super(WeatherState.initial());
}
import
'package:riverpod/riverpod.dart'
;
import
'../di/service_locator.dart'
;
// Provider that uses GetIt
final
weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
return
getIt<WeatherRepository>();
});
final
getWeatherUseCaseProvider = Provider<GetWeatherUseCase>((ref) {
return
getIt<GetWeatherUseCase>();
});
// ViewModel with dependencies from GetIt
final
weatherViewModelProvider = StateNotifierProvider<WeatherViewModel, WeatherState>((ref) {
return
WeatherViewModel(getIt<GetWeatherUseCase>());
});
One of the biggest advantages of dependency injection is testability . GetIt makes it easy to swap real implementations with mocks in tests.
import
'package:test/test.dart'
;
import
'package:get_it/get_it.dart'
;
import
'package:mockito/mockito.dart'
;
class
MockWeatherRepository
extends
Mock
implements
WeatherRepository {}
void
main
() {
late
MockWeatherRepository mockRepository;
late
GetWeatherUseCase useCase;
setUp
(() {
// Reset GetIt before each test
await
GetIt.instance.
reset
();
// Register mock
mockRepository = MockWeatherRepository();
GetIt.instance.
registerSingleton
<WeatherRepository>(mockRepository);
// Get use case with mock injected
useCase = GetIt.instance<GetWeatherUseCase>();
});
test
(
'returns weather data from repository'
, () async {
// Arrange
final
expectedWeather = Weather(
'London'
,
20.0
);
when(mockRepository.
getWeather
(
'London'
))
.thenAnswer((_) async => expectedWeather);
// Act
final
result =
await
useCase.
execute
(
'London'
);
// Assert
expect(result, expectedWeather);
verify(mockRepository.
getWeather
(
'London'
)).
called
(
1
);
});
}
โ Testing Benefits
- Easy mocking โ Replace real implementations with mocks
-
Test isolation
โ
reset()clears all registrations between tests - Constructor injection โ Use optional constructor parameters to inject mocks
- No BuildContext โ Test business logic without UI setup
GetIt supports async initialization for dependencies that need to be set up asynchronously (e.g., opening a database, initializing a client).
final
getIt = GetIt.instance;
void
setupServiceLocator
() async {
// Register async dependencies
getIt.
registerSingletonAsync
<Database>(
()
async {
final
db =
await
Database.
open
();
return
db;
});
// Register dependencies that depend on async ones
getIt.
registerSingletonAsync
<UserRepository>(
()
async {
final
db =
await
getIt.
getAsync
<Database>();
return
UserRepository(db);
}, dependsOn: [Database]);
// Wait for all async registrations to complete
await
getIt.
allReady
();
}
// In main.dart
void
main
() async {
WidgetsFlutterBinding.ensureInitialized();
await
setupServiceLocator
();
runApp(MyApp());
}
Follow these steps to implement dependency injection in your Flutter app:
get_it: ^8.0.2
to
pubspec.yaml
lib/di/service_locator.dart
setupServiceLocator()
before
runApp()
getIt<MyService>()
reset()
and mock dependencies in tests
The
service_role
key bypasses all security policies. Never expose it in your Flutter app.
Always use the
anon
key in your Flutter app. It's safe and respects Row Level Security policies.
Failing to check for error responses or parse error messages correctly leads to confusing user experiences.
Always check the response status code and parse error messages from the API response body.
Hardcoding API URLs makes it difficult to switch between development, staging, and production environments.
Use environment variables or configuration files to manage different API endpoints for different environments.
๐ฏ Key Takeaway
Dependency Injection is essential for building testable, maintainable Flutter apps. Using GetIt as a service locator provides a simple, type-safe way to manage dependencies with O(1) access from anywhere in your app. Register dependencies with the appropriate lifetime (Singleton, LazySingleton, Factory) and use constructor injection for maximum testability.