Skill v1.0.1
currentAutomated scan100/1002 files
version: "1.0.1" name: get-it-expert description: Expert guidance on get_it service locator and dependency injection for Flutter/Dart. Covers registration (singleton, factory, lazy, async), scopes with shadowing, async initialization with init() pattern, retrieval, testing with scope-based mocking, and production patterns. Use when working with get_it, dependency injection, service registration, scopes, or async initialization. metadata: author: flutter-it version: "1.0"
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 immediatelygetIt.registerSingleton<ApiClient>(ApiClient());// Singleton with dispose callbackgetIt.registerSingleton<StreamController>(StreamController(),dispose: (c) => c.close(),);// Lazy singleton - created on first accessgetIt.registerLazySingleton<Database>(() => Database());// Factory - new instance every callgetIt.registerFactory<Logger>(() => Logger());// Factory with parametersgetIt.registerFactoryParam<Logger, String, void>((tag, _) => Logger(tag),);// Named instances - use when registering multiple instances of the same typegetIt.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 initializationgetIt.registerSingletonAsync<DatabaseService>(() => DatabaseService().init(),);// With dependency orderinggetIt.registerSingletonAsync<ApiClient>(() => ApiClient().init(),dependsOn: [DatabaseService],);// Sync factory that needs async dependenciesgetIt.registerSingletonWithDependencies<AppModel>(() => AppModel(getIt<ApiClient>()),dependsOn: [ApiClient],);}
Retrieval
final api = getIt<ApiClient>(); // get<T>() - throws if missingfinal api = getIt.maybeGet<ApiClient>(); // returns null if missingfinal api = await getIt.getAsync<ApiClient>(); // waits for async registrationfinal all = getIt.getAll<PaymentProcessor>(); // all instances of typefinal config = getIt<Config>(instanceName: 'dev'); // named instancefinal 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 scopesawait getIt.popScopesTill('base-scope', inclusive: false);// Drop specific scope by nameawait getIt.dropScope('user-session');// Query scopesgetIt.hasScope('user-session'); // boolgetIt.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 registrationsawait getIt.allReady(timeout: Duration(seconds: 10));// Wait for specific typeawait getIt.isReady<Database>(timeout: Duration(seconds: 5));// Synchronous checks (no waiting)getIt.allReadySync(); // boolgetIt.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 countgetIt.registerSingletonIfAbsent<PageData>(() => PageData(id));// Decrements ref count, disposes only when count reaches 0getIt.releaseInstance<PageData>(ignoreReferenceCount: false);
Utility Methods
getIt.isRegistered<ApiClient>(); // boolgetIt.unregister<ApiClient>(); // remove registrationgetIt.resetLazySingleton<Database>(); // recreate on next accessgetIt.resetLazySingletons(inAllScopes: true); // bulk resetgetIt.checkLazySingletonInstanceExists<Database>(); // is it instantiated?getIt.reset(); // clear everything (for tests)getIt.allowReassignment = true; // allow overwriting registrationsgetIt.enableRegisteringMultipleInstancesOfOneType(); // allow unnamed multiples
Anti-Patterns
// ❌ Accessing async service before allReady()configureDependencies();final db = getIt<Database>(); // THROWS - not ready yet// ✅ Wait firstawait 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 initawait getIt.pushNewScopeAsync(scopeName: 'x',init: (getIt) async { ... },);// OR use synchronous pushNewScope without awaitgetIt.pushNewScope(scopeName: 'x', init: (getIt) { ... });
Testing
// Option 1: Scope-based (preferred) - mocks shadow real registrationssetUp(() {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 scopeawait 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 poppingvoid onLogout() {if (di.hasScope('chat')) di.popScope();if (di.hasScope('auth')) di.popScope();}// ✅ Use popScopesTill to pop everything above (and including) the auth scopeFuture<void> onLogout() async {if (di.hasScope('auth')) {await di.popScopesTill('auth', inclusive: true);}}