skip to content

With get_it, how do registerSingleton, registerLazySingleton and registerFactory differ in when the object is created and how many instances exist?

level: middleimportance: must knowfreq 55%

answer

  1. now, first use, every use
  2. registerSingleton takes a built instance
  3. lazy factory runs once
  4. factory gives a new object per get
  5. async variants and allReady

basics

~20 s

registerSingleton stores an instance you already built, returned on every get; registerLazySingleton runs its factory on the first get and reuses that one object; registerFactory runs its factory on every get, returning a new object each time.

solid answer

~40 s

In get_it, `registerSingleton<T>(instance)` takes an object that already exists, so its cost is paid at startup and every `getIt<T>()` returns it. `registerLazySingleton<T>(() => ...)` stores a factory and runs it only on the first `get`, then caches that single instance, which suits expensive services that may never be used. `registerFactory<T>(() => ...)` runs the factory on **every** `get`, so each caller gets a fresh object; that fits per-screen view models. Singletons and lazy singletons accept a `dispose` callback that `reset()` or `unregister()` calls. Asking for a type that was never registered throws a `StateError`. For objects that need async setup, `registerSingletonAsync` plus `await getIt.allReady()` delays use until they are ready.

code

dart · 44 lines
dart
import 'package:get_it/get_it.dart';

final getIt = GetIt.instance;

class ApiClient {
  ApiClient(this.baseUrl);
  final String baseUrl;
}

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

class CardPaymentService implements PaymentService {
  CardPaymentService(this._api);
  final ApiClient _api;
  @override
  Future<void> charge({required int cents}) async {/* uses _api */}
  @override
  void close() {}
}

class ParkingSessionViewModel {
  ParkingSessionViewModel({required PaymentService payments})
      : _payments = payments;
  final PaymentService _payments;
}

void configureDependencies() {
  // Built now; every get returns this instance.
  getIt.registerSingleton<ApiClient>(ApiClient('https://parking.example'));

  // Built on the first get, then reused.
  getIt.registerLazySingleton<PaymentService>(
    () => CardPaymentService(getIt<ApiClient>()),
    dispose: (service) => service.close(),
  );

  // Built on every get: one view model per screen.
  getIt.registerFactory<ParkingSessionViewModel>(
    () => ParkingSessionViewModel(payments: getIt<PaymentService>()),
  );
}

go deeper

for a junior

Know the three verbs: built now, built on first use, built every time. Pick one for a service and one for a view model.

for a middle

Explain the trade-off: startup cost versus deferred cost versus fresh state, and the StateError and ArgumentError you hit when registrations are wrong.

for a senior

Diagnose lifetime bugs, such as a singleton view model leaking state or a disposed notifier, and design async startup with allReady.

for a principal

Decide which lifetimes the team may use and how startup work is budgeted between eager, lazy and async registrations.

## Three lifetimes get_it is a **service locator**: a registry, usually the global `GetIt.instance` (also `GetIt.I`), where you register how to obtain each type and later call `getIt<T>()`. The registration method decides **when** the object is built and **how many** exist: | Method | Argument | Built | Instances | |---|---|---|---| | `registerSingleton<T>` | an existing instance | before registration, by you | one | | `registerLazySingleton<T>` | a factory function | on the first `get` | one, cached | | `registerFactory<T>` | a factory function | on every `get` | a new one each time | `registerSingleton` also returns the instance you passed, which is handy when the next registration needs it. ## Choosing for a parking app - **`ApiClient`**: cheap to build and used from the first screen, so `registerSingleton` in `main()` is fine. - **`PaymentService`**: initialising the payment SDK is costly and many sessions never reach checkout, so `registerLazySingleton` defers the cost until the first `getIt<PaymentService>()`. - **`ParkingSessionViewModel`**: each screen should start clean, so `registerFactory` hands every caller its own instance. A frequent bug is registering a view model as a singleton: the second visit to the screen sees the previous session's state, and a `ChangeNotifier` that one screen disposed may be handed to the next. ## Options worth knowing - **`dispose`**: `registerSingleton` and `registerLazySingleton` accept a disposing callback that runs when the registration is removed by `reset()`, `unregister()` or popping its scope. - **`instanceName`**: registers several objects of one type under names. - **`registerFactoryParam`**: a factory that takes up to two parameters passed to `get`. - **`registerCachedFactory`**: a factory that holds a weak reference to its last instance and returns it while it is still alive. - **`useWeakReference`** on `registerLazySingleton`, `false` by default. ## When the factory closures run The argument to `registerSingleton` is evaluated **at registration time**, so `registerSingleton<PaymentService>(CardPaymentService(getIt<ApiClient>()))` fails unless `ApiClient` was registered first. The closures given to `registerLazySingleton` and `registerFactory` run later, on `get`, so their own `getIt<T>()` calls only need the other types registered by then. That makes lazy and factory registrations order-independent within a setup function, while eager singletons must be listed in dependency order. A lazy singleton or cached factory whose closure resolves its own type, for example by registering an untyped `getIt.call` tear-off, used to overflow the stack; since get_it 9.3.0 it throws a descriptive `StateError` instead. ## Async setup Some objects need `await` before they are usable. `registerSingletonAsync<T>(() async => ...)` starts the async factory right away, optionally after the types listed in `dependsOn`. Code then waits for readiness: 1. register the async singletons in a setup function; 2. `await getIt.allReady()` before `runApp`, or show a splash while it completes; 3. read them with `getIt<T>()` once ready, or with `getAsync<T>()` to wait. `registerLazySingleton` does not influence `allReady()`; a lazy singleton is built whenever it is first requested. ## Failure modes - **Unregistered type**: `getIt<PaymentService>()` throws a `StateError` saying the type is not registered inside GetIt; `maybeGet` returns `null` instead. - **Double registration**: registering the same type twice in one scope throws an `ArgumentError` unless `allowReassignment` is set. - **Calling the instance**: `GetIt.instance()` with parentheses is a common typo; the error message itself asks whether you meant `GetIt.instance`.

  • When would registerSingletonAsync be the right choice?
    When the object needs an async step before use, such as opening a local database. `registerSingletonAsync` starts the factory immediately (after any `dependsOn` types are ready), and `await getIt.allReady()` before `runApp` makes sure it has finished; `getAsync<T>()` can also wait for one type.
  • What goes wrong if a screen's ChangeNotifier view model is registered with registerLazySingleton?
    Every visit gets the same object, so old state leaks into the next session, and if the widget that used it calls `dispose()`, the next screen receives a disposed notifier and fails when it tries to notify listeners. Per-screen objects belong in `registerFactory`.

saying these in an interview costs you the question

  • registerLazySingleton creates a new instance on every get.
  • registerSingleton takes a factory and builds it on first use.
  • registerFactory caches the first instance and returns it afterwards.
  • getIt<T>() returns null for a type that was never registered.
  • Registering the same type twice simply overwrites the first registration.