- Published on
@injectable in Flutter
- Authors
- Name
- Rosa Tiara
Introduction
Have you ever get stuck passing the same dependencies through multiple widgets, making your code messy and hard to test? Me too! :p In Flutter, there is a term called Dependency Injection (DI) that can help with that. We can use @injectable package.
Think of DI like ordering food. Instead of cooking everything yourself (creating dependencies manually), you tell the waiter what you need, and it's delivered to you. This makes your code cleaner, more flexible, and easier to test.
In this post, we'll see how to use injectable in Flutter to manage dependencies. Let's gooo!
What is Dependency Injection in technical terms?
Dependency Injection is a design pattern where objects receive their dependencies from an external source rather than creating them internally. Let's take a look at the code example.
Without Dependency Injection
class UserService {
final ApiClient apiClient = ApiClient(); // Creating dependency directly
Future<User> getUser(String id) {
return apiClient.get('/users/$id');
}
}
With Dependency Injection
class UserService {
final ApiClient apiClient;
UserService(this.apiClient); // Dependency is injected
Future<User> getUser(String id) {
return apiClient.get('/users/$id');
}
}
The second approach is better because:
- You can easily swap
ApiClientwith a mock for testing - The
UserServicedoesn't need to know how to create anApiClient - Dependencies are explicit and clear
- You can share the same
ApiClientinstance across multiple services
What is Injectable?
injectable is a code generation package for Flutter that automates the setup of dependency injection using the get_it service locator. Instead of manually registering all your dependencies, injectable generates the boilerplate code for you.
Install the package here
Setting Up Injectable
Let's set up injectable in your Flutter project!
1. Add Dependencies
Add these packages to your pubspec.yaml:
dependencies:
get_it: ^7.6.4
injectable: ^2.3.2
dev_dependencies:
injectable_generator: ^2.4.1
build_runner: ^2.4.7
2. Create the Service Locator
Create a file called injection.dart (or di.dart, service_locator.dart) in your lib folder:
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection.config.dart';
final getIt = GetIt.instance;
()
void configureDependencies() => getIt.init();
3. Initialize in Your App
Call configureDependencies() in your main.dart before running the app:
import 'package:flutter/material.dart';
import 'injection.dart';
void main() {
configureDependencies(); // Initialize dependencies
runApp(MyApp());
}
4. Generate the Code
Run the code generator:
flutter pub run build_runner build --delete-conflicting-outputs
This command generates the injection.config.dart file that contains all the registration code.
Using Injectable
Now that we have injectable set up, let's see how to use it in practice.
Basic Registration
To make a class injectable, annotate it with @injectable:
class ApiClient {
Future<Map<String, dynamic>> get(String endpoint) async {
// Implementation
}
}
class UserService {
final ApiClient apiClient;
UserService(this.apiClient);
Future<User> getUser(String id) async {
final data = await apiClient.get('/users/$id');
return User.fromJson(data);
}
}
When you run the code generator, injectable automatically:
- Registers
ApiClientas a singleton (by default) - Registers
UserServiceand injectsApiClientinto it - Creates the necessary factory methods
Using Injected Dependencies
To use your injected dependencies, simply call getIt:
class UserScreen extends StatelessWidget {
Widget build(BuildContext context) {
final userService = getIt<UserService>();
return FutureBuilder<User>(
future: userService.getUser('123'),
builder: (context, snapshot) {
// Build UI
},
);
}
}
Different Registration Types
injectable supports different registration scopes:
Singleton (Default)
class DatabaseService {
// Created once and reused everywhere
}
Lazy Singleton
()
class ExpensiveService {
// Created only when first accessed
}
Factory
()
class TemporaryService {
// New instance created every time it's requested
}
Environment-Specific Registration
You can register different implementations based on the environment:
('dev')
class ApiClient {
// Development implementation
}
('prod')
class ApiClient {
// Production implementation
}
Then set the environment when initializing:
(initializerName: 'init')
void configureDependencies() => getIt.init(environment: 'dev');
Registering Interfaces
When working with interfaces, you need to specify which implementation to use:
abstract class IStorage {
Future<void> save(String key, String value);
}
class SharedPreferencesStorage implements IStorage {
Future<void> save(String key, String value) async {
// Implementation
}
}
// In your injection file, register the interface:
abstract class StorageModule {
(as: IStorage)
SharedPreferencesStorage get storage;
}
Real Example
Let's build a complete example with a user authentication flow:
// 1. Define the API client
class ApiClient {
final http.Client client;
ApiClient(this.client);
Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {
final response = await client.post(
Uri.parse(endpoint),
body: jsonEncode(data),
);
return jsonDecode(response.body);
}
}
// 2. Create authentication service
class AuthService {
final ApiClient apiClient;
final SecureStorage storage;
AuthService(this.apiClient, this.storage);
Future<bool> login(String email, String password) async {
final response = await apiClient.post('/auth/login', {
'email': email,
'password': password,
});
if (response['success'] == true) {
await storage.save('token', response['token']);
return true;
}
return false;
}
}
// 3. Use in your widget
class LoginScreen extends StatefulWidget {
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _authService = getIt<AuthService>();
Future<void> _handleLogin() async {
final success = await _authService.login(
_emailController.text,
_passwordController.text,
);
if (success) {
Navigator.pushReplacementNamed(context, '/home');
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Login failed')),
);
}
}
Widget build(BuildContext context) {
// Build login UI
}
}
Best Practices
1. Organize Your Dependencies
Group related dependencies in modules:
abstract class NetworkModule {
http.Client get httpClient => http.Client();
Dio get dio => Dio();
}
2. Use Interfaces for Abstraction
Always program to interfaces, not implementations:
abstract class IUserRepository {
Future<User> getUser(String id);
}
class UserRepository implements IUserRepository {
// Implementation
}
3. Register Dependencies Early
Initialize dependencies in main() before runApp():
void main() async {
WidgetsFlutterBinding.ensureInitialized();
configureDependencies();
runApp(MyApp());
}
4. Use Named Parameters for Clarity
When a class has multiple dependencies, use named parameters:
class ComplexService {
final ApiClient apiClient;
final DatabaseService database;
final CacheService cache;
ComplexService({
required this.apiClient,
required this.database,
required this.cache,
});
}
5. Keep Registration Simple
Let injectable handle the registration automatically. Only use manual registration when necessary.
Common Pitfalls and Solutions
Pitfall 1: Forgetting to Run Code Generation
Problem: You add @injectable but forget to run build_runner, causing runtime errors.
Solution: Always run flutter pub run build_runner build --delete-conflicting-outputs after adding new injectable classes.
Pitfall 2: Circular Dependencies
Problem: Two classes depend on each other, causing initialization issues.
class ServiceA {
final ServiceB serviceB;
ServiceA(this.serviceB);
}
class ServiceB {
final ServiceA serviceA; // Circular dependency!
ServiceB(this.serviceA);
}
Solution: Refactor to break the cycle, often by introducing an interface or a third service:
abstract class IServiceA {
void doSomething();
}
class ServiceA implements IServiceA {
final ServiceB serviceB;
ServiceA(this.serviceB);
}
class ServiceB {
final IServiceA serviceA; // Use interface instead
ServiceB(this.serviceA);
}
Pitfall 3: Not Using LazySingleton for Heavy Objects
Problem: Creating expensive objects immediately, even if they're never used.
Solution: Use @LazySingleton() for services that are expensive to create:
()
class DatabaseService {
DatabaseService() {
// Expensive initialization
}
}
Pitfall 4: Accessing getIt Before Initialization
Problem: Trying to use getIt before calling configureDependencies().
Solution: Always initialize dependencies in main() before using them.
Testing with Injectable
One of the biggest benefits of dependency injection is easier testing. You can easily swap real implementations with mocks:
// In your test file
void main() {
setUp(() {
// Reset getIt before each test
getIt.reset();
// Register mock implementations
getIt.registerLazySingleton<ApiClient>(() => MockApiClient());
getIt.registerLazySingleton<UserService>(() => UserService(getIt()));
});
test('should get user successfully', () async {
final userService = getIt<UserService>();
final user = await userService.getUser('123');
expect(user.id, '123');
});
}
Takeaways
Dependency injection with injectable makes your Flutter code:
- More Testable: Easy to swap real implementations with mocks
- More Maintainable: Clear dependencies and separation of concerns
- More Flexible: Easy to change implementations without modifying dependent classes
- Less Coupled: Classes don't need to know how to create their dependencies
Best Practices Checklist
Before implementing injectable in your project, remember:
- Have you added all necessary dependencies to
pubspec.yaml? - Did you create the
injection.dartfile with@InjectableInit()? - Are you initializing dependencies in
main()beforerunApp()? - Have you run
build_runnerto generate the configuration? - Are you using interfaces for better abstraction?
- Have you organized related dependencies into modules?
Got questions about injectable or dependency injection in Flutter? Drop them in the comments below!