Lesson 41 of 51 advanced

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

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. 1. Import the injectable package for annotations
  2. 2. Import GetIt for the service locator
  3. 3. Import the generated config file that contains all registrations
  4. 4.
  5. 5. Comment: The setup file that initializes everything
  6. 6. Create a global GetIt instance that the whole app shares
  7. 7.
  8. 8. The @InjectableInit annotation tells code gen to generate init() here
  9. 9. configureDependencies calls the generated init method on our GetIt instance
  10. 10.
  11. 11. Comment: Domain layer defines WHAT we need (abstract contract)
  12. 12. Abstract class AuthRepository -- no implementation details
  13. 13. A login method signature -- takes email and password, returns a User
  14. 14. A logout method signature -- clears the session
  15. 15. Closing the abstract class
  16. 16.
  17. 17. Comment: Data layer defines HOW we do it (concrete implementation)
  18. 18. @LazySingleton(as: AuthRepository) registers this as the AuthRepository implementation
  19. 19. AuthRepositoryImpl class implements the AuthRepository interface
  20. 20. Private field for the API service (injected by GetIt)
  21. 21. Private field for token storage (injected by GetIt)
  22. 22.
  23. 23. Constructor receives both dependencies -- GetIt resolves them automatically
  24. 24.
  25. 25. @override marks this as implementing the abstract login method
  26. 26. Login method makes an API call with email and password
  27. 27. Sends a POST request to /auth/login with credentials
  28. 28. Closing the request data map
  29. 29. Saves the tokens from the response to secure storage
  30. 30. Parses and returns the User from the response JSON
  31. 31. Closing the login method
  32. 32.
  33. 33. @override marks this as implementing the abstract logout method
  34. 34. Logout method clears stored tokens
  35. 35. Closing the logout method
  36. 36. Closing AuthRepositoryImpl
  37. 37.
  38. 38. Comment: Use case layer -- gets a fresh instance each time (factory)
  39. 39. @injectable registers LoginUseCase as a factory
  40. 40. LoginUseCase depends on AuthRepository (resolved by GetIt)
  41. 41. Constructor receives the repository
  42. 42.
  43. 43. The call method delegates to the repository login
  44. 44. Closing LoginUseCase
  45. 45.
  46. 46. Comment: Module for third-party dependencies
  47. 47. @module tells injectable this class provides external dependencies
  48. 48. Abstract class RegisterModule
  49. 49. @singleton means one Dio instance for the whole app
  50. 50. Creates Dio with base URL and timeout configuration
  51. 51. Closing the BaseOptions and Dio constructor
  52. 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

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart