get-it-expert
get_it Expert - Service Locator & Dependency Injection
What: Type-safe service locator with O(1) lookup. Register services globally, retrieve anywhere without BuildContext. Pure Dart, no code generation.
CRITICAL RULES
- Register all services BEFORE
runApp() pushNewScope()is synchronous. UsepushNewScopeAsync()for async initpopScope()IS async (returnsFuture<void>)allReady()returnsFuture<void>- await it or use FutureBuilder/watch_it- Dispose callbacks are a parameter on registration methods, not separate methods
- Once async singletons are initialized (after
allReady()), access them with normalgetIt<T>()- nogetAsyncneeded - If using watch_it, a global
dialias forGetIt.Iis already provided - usedi<T>()instead ofgetIt<T>()
Registration
final getIt = GetIt.instance;
void configureDependencies() {
// Singleton - created immediately
getIt.registerSingleton<ApiClient>(ApiClient());
// Singleton with dispose callback
getIt.registerSingleton<StreamController>(
StreamController(),
dispose: (c) => c.close(),
);
// Lazy singleton - created on first access
getIt.registerLazySingleton<Database>(() => Database());
// Factory - new instance every call
getIt.registerFactory<Logger>(() => Logger());
// Factory with parameters
getIt.registerFactoryParam<Logger, String, void>(
(tag, _) => Logger(tag),
);
// Named instances - use when registering multiple instances of the same type
getIt.registerSingleton<Config>(devConfig, instanceName: 'dev');
getIt.registerSingleton<Config>(prodConfig, instanceName: 'prod');
}
Async Initialization
Preferred pattern: Give services a Future<T> init() method that returns this. This keeps initialization logic inside the class and allows concise registration:
class DatabaseService {
late final Database _db;
Future<DatabaseService> init() async {
_db = await Database.open('app.db');
return this; // Always return this
}
}
void configureDependencies() {
// init() pattern - concise, self-contained initialization
getIt.registerSingletonAsync<DatabaseService>(
() => DatabaseService().init(),
);
// With dependency ordering
getIt.registerSingletonAsync<ApiClient>(
() => ApiClient().init(),
dependsOn: [DatabaseService],
);
// Sync factory that needs async dependencies
getIt.registerSingletonWithDependencies<AppModel>(
() => AppModel(getIt<ApiClient>()),
dependsOn: [ApiClient],
);
}
Retrieval
final api = getIt<ApiClient>(); // get<T>() - throws if missing
final api = getIt.maybeGet<ApiClient>(); // returns null if missing
final api = await getIt.getAsync<ApiClient>(); // waits for async registration
final all = getIt.getAll<PaymentProcessor>(); // all instances of type
final config = getIt<Config>(instanceName: 'dev'); // named instance
final logger = getIt<Logger>(param1: 'MyClass'); // factory with params
Scopes
// Push scope (synchronous init)
getIt.pushNewScope(
scopeName: 'user-session',
init: (getIt) {
getIt.registerSingleton<UserData>(currentUser);
getIt.registerLazySingleton<UserPrefs>(() => UserPrefs(currentUser.id));
},
);
// Push scope (async init)
await getIt.pushNewScopeAsync(
scopeName: 'user-session',
init: (getIt) async {
final prefs = await UserPrefs.load(currentUser.id);
getIt.registerSingleton<UserPrefs>(prefs);
},
);
// Pop scope (always async - calls dispose callbacks)
await getIt.popScope();
// Pop multiple scopes
await getIt.popScopesTill('base-scope', inclusive: false);
// Drop specific scope by name
await getIt.dropScope('user-session');
// Query scopes
getIt.hasScope('user-session'); // bool
getIt.currentScopeName; // String?
Scope shadowing: Scopes are a stack of registration layers. When you register a type in a new scope that already exists in a lower scope, the new registration shadows (hides) the original. getIt<T>() always searches top-down, returning the first match. Popping a scope removes its registrations and restores access to the shadowed ones below. This is what makes scopes useful for testing (push a scope with mocks, pop it in tearDown), for user sessions (push user-specific services that shadow defaults), and for grouping related objects that should be disposed together based on business logic (e.g., push a scope for a shopping cart - popping it disposes all cart-related services at once).
Ready State
// Wait for ALL async registrations
await getIt.allReady(timeout: Duration(seconds: 10));
// Wait for specific type
await getIt.isReady<Database>(timeout: Duration(seconds: 5));
// Synchronous checks (no waiting)
getIt.allReadySync(); // bool
getIt.isReadySync<Database>(); // bool
UI integration: Use FutureBuilder with getIt.allReady() to show a splash screen while async services initialize. If using watch_it, prefer its allReady() function inside a WatchingWidget instead (see watch-it-expert skill).
Reference Counting
For scenarios like recursive navigation (same page pushed multiple times):
// Registers only if not already registered, increments ref count
getIt.registerSingletonIfAbsent<PageData>(() => PageData(id));
// Decrements ref count, disposes only when count reaches 0
getIt.releaseInstance<PageData>(ignoreReferenceCount: false);
Utility Methods
getIt.isRegistered<ApiClient>(); // bool
getIt.unregister<ApiClient>(); // remove registration
getIt.resetLazySingleton<Database>(); // recreate on next access
getIt.resetLazySingletons(inAllScopes: true); // bulk reset
getIt.checkLazySingletonInstanceExists<Database>(); // is it instantiated?
getIt.reset(); // clear everything (for tests)
getIt.allowReassignment = true; // allow overwriting registrations
getIt.enableRegisteringMultipleInstancesOfOneType(); // allow unnamed multiples
Anti-Patterns
// ❌ Accessing async service before allReady()
configureDependencies();
final db = getIt<Database>(); // THROWS - not ready yet
// ✅ Wait first
await getIt.allReady();
final db = getIt<Database>(); // Safe
// ❌ await on pushNewScope (it's void, not Future)
await getIt.pushNewScope(scopeName: 'x'); // Won't compile
// ✅ Use pushNewScopeAsync for async init
await getIt.pushNewScopeAsync(
scopeName: 'x',
init: (getIt) async { ... },
);
// OR use synchronous pushNewScope without await
getIt.pushNewScope(scopeName: 'x', init: (getIt) { ... });
Testing
// Option 1: Scope-based (preferred) - mocks shadow real registrations
setUp(() {
GetIt.I.pushNewScope(
init: (getIt) {
getIt.registerSingleton<ApiClient>(MockApiClient());
},
);
});
tearDown(() async {
await GetIt.I.popScope();
});
// Option 2: Hybrid constructor injection (optional convenience)
class MyService {
final ApiClient api;
MyService({ApiClient? api}) : api = api ?? getIt<ApiClient>();
}
// Test: MyService(api: MockApiClient())
Production Patterns
Two-phase DI (base + throwable scope):
void setupBaseServices() {
di.registerSingleton<ApiClient>(createApiClient());
di.registerSingleton<CacheManager>(WcImageCacheManager());
}
Future<void> setupThrowableScope() async {
di.pushNewScope(scopeName: 'throwableScope');
di.registerLazySingletonAsync<StoryManager>(
() async => StoryManager().init(),
dispose: (m) => m.dispose(),
dependsOn: [UserManager],
);
}
// On error recovery: reset throwable scope
await di.popScopesTill('throwableScope', inclusive: true);
await setupThrowableScope();
Logout / scope cleanup — use popScopesTill to pop multiple scopes at once instead of manually checking and popping each one:
// ❌ Manual scope-by-scope popping
void onLogout() {
if (di.hasScope('chat')) di.popScope();
if (di.hasScope('auth')) di.popScope();
}
// ✅ Use popScopesTill to pop everything above (and including) the auth scope
Future<void> onLogout() async {
if (di.hasScope('auth')) {
await di.popScopesTill('auth', inclusive: true);
}
}
More from flutter-it/flutter_it
flutter-it
Overview of the flutter_it construction set - modular Flutter packages (get_it, watch_it, command_it, listen_it) that work standalone or together. Use when deciding which flutter_it package to use, understanding package dependencies, or getting oriented with the ecosystem.
6listen-it-expert
Expert guidance on listen_it ValueListenable operators and reactive collections for Flutter/Dart. Covers listen(), transformation operators (map, select, where, debounce, async), combining operators (combineLatest, mergeWith), operator chaining, reactive collections (ListNotifier, MapNotifier, SetNotifier), transactions, and CustomValueNotifier. Use when working with ValueListenable transformations, reactive data pipelines, or listen_it collections.
6watch-it-expert
Expert guidance on watch_it reactive widget state management for Flutter. Covers watch functions (watch, watchIt, watchValue, watchStream, watchFuture), handler registration (registerHandler, registerStreamHandler, registerFutureHandler), lifecycle functions (callOnce, createOnce, onDispose), ordering rules, widget granularity, and startup orchestration. Use when building reactive widgets with watch_it, watching ValueListenables/Streams/Futures, or managing widget-scoped state.
6feed-datasource-expert
Expert guidance on implementing paginated feeds and infinite scroll in Flutter using FeedDataSource and PagedFeedDataSource patterns. Covers base feed data source, cursor-based pagination, auto-pagination at length-3, proxy lifecycle with reference counting, feed widget implementation, filtered feeds, event bus integration, and creation with createOnce. Use when building paginated lists, infinite scroll, feed views, or managing proxy lifecycle in feeds.
6command-it-expert
Expert guidance on command_it command pattern for Flutter. Covers command creation (async, sync, undoable, with progress), execution, observable properties (isRunning, canRun, errors, results), restrictions, error handling with ErrorFilter/ErrorReaction, global error stream, and production patterns. Use when working with command_it commands, async operations with loading/error states, or command restrictions.
6flutter-architecture-expert
Architecture guidance for Flutter apps using the flutter_it construction set (get_it, watch_it, command_it, listen_it). Covers Pragmatic Flutter Architecture (PFA) with Services/Managers/Views, feature-based project structure, manager pattern, proxy pattern with optimistic updates and override fields, DataRepository with reference counting, scoped services, widget granularity, testing, and best practices. Use when designing app architecture, structuring Flutter projects, implementing managers or proxies, or planning feature organization.
6