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.