skills/get-it-expert/SKILL.md
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.
npx skillsauth add flutter-it/watch_it get-it-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
What: Type-safe service locator with O(1) lookup. Register services globally, retrieve anywhere without BuildContext. Pure Dart, no code generation.
runApp()pushNewScope() is synchronous. Use pushNewScopeAsync() for async initpopScope() IS async (returns Future<void>)allReady() returns Future<void> - await it or use FutureBuilder/watch_itallReady()), access them with normal getIt<T>() - no getAsync neededdi alias for GetIt.I is already provided - use di<T>() instead of getIt<T>()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');
}
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],
);
}
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
// 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).
// 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).
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);
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
// ❌ 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) { ... });
// 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())
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);
}
}
development
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.
testing
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.
development
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.
testing
Create, edit, improve, or audit AgentSkills. Use when creating a new skill from scratch or when asked to improve, review, audit, tidy up, or clean up an existing skill or SKILL.md file. Also use when editing or restructuring a skill directory (moving files to references/ or scripts/, removing stale content, validating against the AgentSkills spec). Triggers on phrases like "create a skill", "author a skill", "tidy up a skill", "improve this skill", "review the skill", "clean up the skill", "audit the skill".