skip to content

With the injectable package, how do annotations such as @Injectable(as:), @lazySingleton and @Environment become get_it registrations?

level: middleimportance: should knowfreq 28%

answer

  1. codegen on top of get_it
  2. @InjectableInit on configureDependencies
  3. generated .config.dart with init()
  4. as: binds the abstract type
  5. init(environment:) filters by env

basics

~20 s

injectable's generator reads annotated classes and writes a .config.dart file whose init() calls get_it's register methods: @injectable becomes a factory, @singleton and @lazySingleton their get_it counterparts, as: binds an abstract type, and @Environment limits a class to matching environments.

solid answer

~30 s

injectable removes hand-written get_it setup. You annotate a top-level `configureDependencies()` with `@InjectableInit()` and call the generated `getIt.init()` inside it; injectable_generator, a dev dependency run through build_runner, writes `<file>.config.dart`. Each annotated class becomes a registration: `@injectable` a factory, `@singleton` a `registerSingleton`, `@lazySingleton` a `registerLazySingleton`, with constructor parameters resolved from get_it. `@LazySingleton(as: PaymentService)` registers the class under the abstract type. `@Environment('test')`, or the shipped `dev`, `prod` and `test` constants, marks a class for some environments, and `init(environment: Environment.test)` registers only classes with no environment or a matching one, so a `FakePaymentService` can replace the real service in a test build.

code

dart · 32 lines
dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';

import 'di.config.dart'; // generated by injectable_generator

final getIt = GetIt.instance;

@InjectableInit()
void configureDependencies(String env) => getIt.init(environment: env);

abstract class PaymentService {
  Future<void> charge({required int cents});
}

@LazySingleton(as: PaymentService, env: [Environment.prod, Environment.dev])
class CardPaymentService implements PaymentService {
  @override
  Future<void> charge({required int cents}) async {/* real API */}
}

@LazySingleton(as: PaymentService, env: [Environment.test])
class FakePaymentService implements PaymentService {
  final charges = <int>[];
  @override
  Future<void> charge({required int cents}) async => charges.add(cents);
}

@injectable
class ParkingSessionViewModel {
  ParkingSessionViewModel(this._payments);
  final PaymentService _payments;
}

go deeper

for a junior

Know that injectable generates get_it registration code from annotations, so you still call getIt<T>().

for a middle

Map each annotation to its get_it call and explain how as: and @Environment let a test build register a fake.

for a senior

Judge when codegen is worth its build time and stale-file failures, and how to keep environment tags from drifting.

for a principal

Decide whether the app's dependency graph is large enough to justify generated wiring and how to migrate to or away from it.

## What injectable adds get_it registrations are plain code, and in a large app `configureDependencies()` grows to hundreds of lines that must be kept in step with every constructor. **injectable** generates that code from annotations. It is two packages: - `injectable`, a normal dependency, holding the annotations; - `injectable_generator`, a dev dependency, run through build_runner. The output is still get_it: the generated code calls get_it's register methods, so everything about lifetimes and `getIt<T>()` still applies. ## Setup 1. Create `final getIt = GetIt.instance;`. 2. Write a top-level function annotated `@InjectableInit()` whose body calls `getIt.init()`. 3. Import the generated `<file>.config.dart`, named after the file that holds the annotated function. 4. Call `configureDependencies()` in `main()` before `runApp`. `@InjectableInit` defaults include `initializerName: 'init'` and `asExtension: true`, which is why the call reads `getIt.init()`. ## Annotation to registration | Annotation | Generated get_it call | |---|---| | `@injectable` / `@Injectable()` | factory: a new instance per get | | `@singleton` / `@Singleton()` | `registerSingleton` | | `@lazySingleton` / `@LazySingleton()` | `registerLazySingleton` | | `as: SomeAbstractType` | registered under that type instead of the class | | `@Named('x')` | registered, or injected, under a name | | `@module` abstract class | registrations for third-party types you cannot annotate | | `@preResolve` | the future is awaited before registration | Constructor parameters are resolved from get_it in the generated code, so `ParkingSessionViewModel(PaymentService payments)` gets whatever is registered as `PaymentService`. ## What the generated file contains The generated `init` extension builds a small helper object from the injectable package, `GetItHelper`, and calls one method per annotated class: `factory`, `singleton` or `lazySingleton`, each wrapping the matching get_it registration. Constructor parameters become `gh<T>()` lookups inside the closures. Because it is ordinary Dart, you can open the `.config.dart` file to see exactly which type a class was registered under and in which environments; that is the first place to look when `getIt<PaymentService>()` throws a `StateError` in a test build. ## Environments: the test swap `@Environment('name')` restricts a class to some environments, and the package ships `dev`, `prod` and `test` constants. The `env:` field on `@Injectable`, `@Singleton` and `@LazySingleton` does the same. At startup, `getIt.init(environment: ...)` builds a filter that registers classes with **no environment, or a matching one**. In a parking app: - `CardPaymentService` is `@LazySingleton(as: PaymentService, env: [Environment.prod, Environment.dev])`; - `FakePaymentService` is `@LazySingleton(as: PaymentService, env: [Environment.test])`; - `ParkingSessionViewModel` is `@injectable` with no environment, so it exists everywhere. Calling `init(environment: Environment.test)` registers the fake as `PaymentService`; production calls it with `Environment.prod`. Custom filters such as `NoEnvOrContainsAll` exist through the `environmentFilter` parameter. ## Costs to weigh - A generated file must be regenerated after every annotation or constructor change; a stale one fails at runtime with a missing registration. - Build time grows with the number of annotated classes. - `@preResolve` makes the generated `init` asynchronous, so `configureDependencies` must return a future and `main` must await it. - Wiring errors still surface at runtime, as get_it errors, not as compile errors. For a small app, hand-written registrations or plain constructor calls are often clearer; injectable pays off when the graph is large and changes often.

  • A class has no @Environment annotation. Is it registered when init is called with Environment.test?
    Yes. The default filter created from `init(environment: ...)` registers dependencies that have no environment, or one that matches. Only classes tagged solely with other environments are skipped.
  • How do you register a third-party class you cannot annotate?
    Declare an abstract class annotated `@module` with getters or methods returning the type, annotated with `@singleton`, `@lazySingleton` or `@injectable`. Add `@preResolve` when the value comes from a future that must be awaited before registration.

saying these in an interview costs you the question

  • injectable is a separate container that replaces get_it at runtime.
  • Missing registrations are caught at compile time by injectable.
  • @injectable registers a singleton by default.
  • Classes without @Environment are skipped whenever an environment is passed.
  • The generated file never needs regenerating after constructor changes.