Injectable & Code Generation
Auto-Wiring Your Dependencies
Open interactive version (quiz + challenge)Real-world analogy
Imagine you run a restaurant and instead of manually telling each waiter which chef to talk to, you put color-coded stickers on everyone. Red sticker chefs only talk to red sticker waiters. Injectable is like those stickers -- you annotate your classes and the system auto-wires everything together!
What is it?
Injectable is a code generation package for Dart that works with GetIt. You annotate your classes with @injectable, @singleton, or @lazySingleton, then run build_runner to auto-generate all the GetIt registration code. It eliminates manual dependency registration and ensures your dependency graph is always correct and complete.
Real-world relevance
In the team_mvp_kit project, injectable auto-registers over 30 classes: repositories, use cases, BLoCs, services, and third-party dependencies. Without it, you would need a massive manual setup file that breaks every time you add a new class. Code generation keeps your DI configuration in sync with your actual code automatically.
Key points
- What is Code Generation? — Code generation means writing code that writes MORE code for you. Instead of manually registering every service in GetIt, you add annotations like @injectable and run build_runner to auto-generate the registration code. Less boilerplate, fewer mistakes.
- The @injectable Annotation — Mark a class with @injectable and it gets registered as a factory in GetIt automatically. Every time you request it, you get a NEW instance. This is perfect for use cases, BLoCs, and anything that should not share state between screens.
- The @singleton Annotation — Mark a class with @singleton and only ONE instance is created for the entire app lifetime. It is created immediately when the injector initializes. Use this for services that must maintain state or expensive-to-create objects like database connections.
- The @lazySingleton Annotation — Like @singleton but the instance is NOT created until the first time someone requests it. This saves memory and startup time for services that may not be needed right away. In team_mvp_kit, most repository implementations use @LazySingleton.
- Registering as Abstract Type — Use @Injectable(as: AbstractType) to register an implementation against its abstract interface. This is critical for Clean Architecture -- your domain layer depends on the abstract repository, and the data layer provides the concrete implementation.
- The @module Annotation — Some classes like Dio or SharedPreferences come from external packages and you cannot annotate them with @injectable. Use a @module class to register third-party dependencies. Methods annotated with @singleton or @lazySingleton provide these instances.
- build_runner & Code Generation — Run 'dart run build_runner build' to generate the injection config file. This scans all your @injectable annotations and creates a .config.dart file with all the GetIt registrations. Run it after adding or changing any annotated class.
- The Injection Setup in team_mvp_kit — The team_mvp_kit uses a configureDependencies() function that calls the generated init() on the global GetIt instance. This single function initializes ALL dependencies in the correct order, handling async dependencies with @preResolve.
- @preResolve for Async Dependencies — Some dependencies need async initialization like opening a Hive box or getting SharedPreferences. Mark them with @preResolve and they will be awaited during configureDependencies(). The generated init() function becomes async automatically.
- Environment-Specific Registration — Use @Environment('dev') or @Environment('prod') to register different implementations for different build modes. For example, register a MockApiService in development and a real ApiService in production. The environment is set when calling configureDependencies.
Code example
import 'package:injectable/injectable.dart';
import 'package:get_it/get_it.dart';
import 'injection.config.dart';
// 1. Setup file (injection.dart)
final getIt = GetIt.instance;
@InjectableInit()
Future<void> configureDependencies() async => getIt.init();
// 2. Domain layer - abstract repository
abstract class AuthRepository {
Future<User> login(String email, String password);
Future<void> logout();
}
// 3. Data layer - concrete implementation
@LazySingleton(as: AuthRepository)
class AuthRepositoryImpl implements AuthRepository {
final ApiService _api;
final TokenStorage _tokenStorage;
AuthRepositoryImpl(this._api, this._tokenStorage);
@override
Future<User> login(String email, String password) async {
final response = await _api.post('/auth/login', data: {
'email': email,
'password': password,
});
await _tokenStorage.saveTokens(response['tokens']);
return User.fromJson(response['user']);
}
@override
Future<void> logout() async {
await _tokenStorage.clearTokens();
}
}
// 4. Use case - auto-registered as factory
@injectable
class LoginUseCase {
final AuthRepository _repo;
LoginUseCase(this._repo);
Future<User> call(String email, String password) =>
_repo.login(email, password);
}
// 5. Third-party module
@module
abstract class RegisterModule {
@singleton
Dio get dio => Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 15),
));
}Line-by-line walkthrough
- 1. Import the injectable package for annotations
- 2. Import GetIt for the service locator
- 3. Import the generated config file that contains all registrations
- 4.
- 5. Comment: The setup file that initializes everything
- 6. Create a global GetIt instance that the whole app shares
- 7.
- 8. The @InjectableInit annotation tells code gen to generate init() here
- 9. configureDependencies calls the generated init method on our GetIt instance
- 10.
- 11. Comment: Domain layer defines WHAT we need (abstract contract)
- 12. Abstract class AuthRepository -- no implementation details
- 13. A login method signature -- takes email and password, returns a User
- 14. A logout method signature -- clears the session
- 15. Closing the abstract class
- 16.
- 17. Comment: Data layer defines HOW we do it (concrete implementation)
- 18. @LazySingleton(as: AuthRepository) registers this as the AuthRepository implementation
- 19. AuthRepositoryImpl class implements the AuthRepository interface
- 20. Private field for the API service (injected by GetIt)
- 21. Private field for token storage (injected by GetIt)
- 22.
- 23. Constructor receives both dependencies -- GetIt resolves them automatically
- 24.
- 25. @override marks this as implementing the abstract login method
- 26. Login method makes an API call with email and password
- 27. Sends a POST request to /auth/login with credentials
- 28. Closing the request data map
- 29. Saves the tokens from the response to secure storage
- 30. Parses and returns the User from the response JSON
- 31. Closing the login method
- 32.
- 33. @override marks this as implementing the abstract logout method
- 34. Logout method clears stored tokens
- 35. Closing the logout method
- 36. Closing AuthRepositoryImpl
- 37.
- 38. Comment: Use case layer -- gets a fresh instance each time (factory)
- 39. @injectable registers LoginUseCase as a factory
- 40. LoginUseCase depends on AuthRepository (resolved by GetIt)
- 41. Constructor receives the repository
- 42.
- 43. The call method delegates to the repository login
- 44. Closing LoginUseCase
- 45.
- 46. Comment: Module for third-party dependencies
- 47. @module tells injectable this class provides external dependencies
- 48. Abstract class RegisterModule
- 49. @singleton means one Dio instance for the whole app
- 50. Creates Dio with base URL and timeout configuration
- 51. Closing the BaseOptions and Dio constructor
- 52. Closing RegisterModule
Spot the bug
@LazySingleton(as: AuthRepository)
class AuthRepositoryImpl {
final ApiService _api;
AuthRepositoryImpl(this._api);
Future<User> login(String email, String password) async {
return _api.post('/auth/login');
}
}Need a hint?
The class is registered as AuthRepository but look at what it actually declares...
Show answer
AuthRepositoryImpl is registered as: AuthRepository but does not implement it. Add 'implements AuthRepository' to the class declaration: 'class AuthRepositoryImpl implements AuthRepository'. Without this, the code generator will throw an error because the type does not match.
Explain like I'm 5
Imagine you have a huge LEGO set with 200 pieces. Instead of reading the manual to figure out which piece connects where, you put a tiny label on each piece that says 'I connect to the blue piece' and a robot reads all the labels and snaps everything together for you. That is what injectable does -- it reads your labels (annotations) and connects all your code pieces automatically!
Fun fact
The injectable package was inspired by Angular's dependency injection system. The creator, Milad Akarie, wanted to bring the same auto-wiring magic to Flutter. It now has over 2000 GitHub stars and is one of the most popular DI solutions in the Flutter ecosystem!
Hands-on challenge
Create a simple injectable setup: define an abstract UserRepository, implement it with @LazySingleton(as: UserRepository), create a @injectable GetUsersUseCase that depends on it, and a @module for Dio. Run build_runner and check the generated .config.dart file.
More resources
- injectable Package (pub.dev)
- GetIt Package (pub.dev)
- build_runner Documentation (pub.dev)