What is Dependency Injection?
Dependency Injection is a technique where an object receives its dependencies from an external source rather than creating them itself. This promotes loose coupling and enhances code flexibility.
Types of Dependency Injection
- Constructor Injection: Passing dependencies via a constructor.
- Setter Injection: Providing dependencies through setter methods.
- Method Injection: Supplying dependencies as method parameters.
In Flutter, constructor injection is the most common approach due to its simplicity and alignment with Dart’s object-oriented nature.
Why Use Dependency Injection in Flutter?
- Modularity: Swap implementations (e.g., mock services for testing) without changing the core logic.
- Testability: Easily mock dependencies for unit tests.
- Scalability: Manage complex dependency trees in large apps.
- Maintainability: Reduce tight coupling between classes.
Popular DI Solutions in Flutter
Several packages simplify DI in Flutter:
-
get_it
: A lightweight service locator, easy to set up and widely used. -
provider
: Often used for state management but can handle DI. -
injectable
: A code-generator-based DI solution for advanced setups. -
kiwi
: Another service locator with a focus on simplicity.
This blog focuses on get_it
due to its popularity and ease of use.
Setting Up get_it
for Dependency Injection
Let’s walk through implementing DI in a Flutter app using get_it
. We’ll build a simple app that fetches and displays user data from a mock API.
Step 1: Add Dependencies
Add get_it
to your pubspec.yaml
:
dependencies:
get_it: ^8.2.0
Run flutter pub get
to install the package.
Step 2: Create a Service Locator
Set up a singleton instance of GetIt
to register and access dependencies.
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
void setupDependencies() {
getIt.registerSingleton<UserService>(UserService());
getIt.registerFactory<UserRepository>(() => UserRepository(getIt<UserService>()));
}
-
registerSingleton
: Creates a single instance reused throughout the app. -
registerFactory
: Creates a new instance each time the dependency is requested. -
registerLazySingleton
: Creates a single instance only when first requested.
Step 3: Define Services and Repositories
Create a simple service and repository to demonstrate DI.
// user_service.dart
class UserService {
Future<String> fetchUserName() async {
// Simulate API call
await Future.delayed(const Duration(seconds: 1));
return 'John Doe';
}
}
// user_repository.dart
class UserRepository {
final UserService userService;
UserRepository(this.userService);
Future<String> getUserName() async {
return userService.fetchUserName();
}
}
Step 4: Integrate DI in Your Flutter App
Use the registered dependencies in a Flutter widget.
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
void main() {
setupDependencies(); // Initialize dependencies
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const UserScreen(),
);
}
}
class UserScreen extends StatefulWidget {
const UserScreen({super.key});
@override
_UserScreenState createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
String _userName = 'Loading...';
@override
void initState() {
super.initState();
_loadUserName();
}
Future<void> _loadUserName() async {
final repository = getIt<UserRepository>();
final name = await repository.getUserName();
setState(() {
_userName = name;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User Profile')),
body: Center(
child: Semantics(
label: 'User name display',
child: Text(
'User: $_userName',
style: const TextStyle(fontSize: 20),
),
),
),
);
}
}
Explanation
-
Service Locator:
getIt
holds theUserService
andUserRepository
instances. -
Dependency Injection:
UserRepository
receivesUserService
via constructor injection. -
Widget Integration: The
UserScreen
widget accessesUserRepository
usinggetIt<UserRepository>()
to fetch data.
Best Practices for Dependency Injection
-
Centralize Registration: Define all dependencies in a single
setupDependencies
function for clarity. - Use Singleton for Services: Register services like APIs or databases as singletons to avoid redundant instances.
-
Test with Mocks: Replace real services with mocks during testing using
getIt.reset()
orgetIt.registerSingleton<MockService>
. -
Keep It Simple: Avoid overcomplicating DI setups for small apps; use
get_it
orprovider
for straightforward needs. -
Combine with State Management: Integrate DI with state management solutions like
provider
orRiverpod
for cohesive architecture.
Common Pitfalls
- Overusing Singletons: Avoid registering everything as a singleton; use factories for objects that need fresh instances.
- Circular Dependencies: Ensure dependencies don’t create cycles, causing runtime errors.
Top comments (0)