/get-it-expert
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
$ npx -y skills add flutter-it/get_it --skill get-it-expert --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/get-it-expert
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
get-it-expert.SKILL.mdname: 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. Use `pushNewScopeAsync()` for async init
- `popScope()` IS async (returns `Future<void>`)
- `allReady()` returns `Future<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 normal `getIt<T>()` - no `getAsync` needed
- If using watch_it, a global `di` alias for `GetIt.I` is already provided - use `di<T>()` instead of `getIt<T>()`
- NEVER register an untyped tear-off as a factory: `registerLazySingleton<Iface>(getIt.call)` re-enters `get<Iface>()`. Since 9.3.0 lazy singletons (sync/async) and cached factories throw a descriptive `StateError` for circular self-resolution instead of a StackOverflowError; plain factories are not guarded (recursive factories are legitimate). Use `() => getIt<Impl>()`
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),
);
// Cached factory with parameters - same params return the same instance
// while it is still referenced (weak reference); watchable with watch_it 2.5.0+
getIt.registerCachedFactoryParam<StationManager, String, void>(
(id, _) => StationManager(id),
);
// 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<
Read more
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. Use `pushNewScopeAsync()` for async init
- `popScope()` IS async (returns `Future<void>`)
- `allReady()` returns `Future<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 normal `getIt<T>()` - no `getAsync` needed
- If using watch_it, a global `di` alias for `GetIt.I` is already provided - use `di<T>()` instead of `getIt<T>()`
- NEVER register an untyped tear-off as a factory: `registerLazySingleton<Iface>(getIt.call)` re-enters `get<Iface>()`. Since 9.3.0 lazy singletons (sync/async) and cached factories throw a descriptive `StateError` for circular self-resolution instead of a StackOverflowError; plain factories are not guarded (recursive factories are legitimate). Use `() => getIt<Impl>()`
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),
);
// Cached factory with parameters - same params return the same instance
// while it is still referenced (weak reference); watchable with watch_it 2.5.0+
getIt.registerCachedFactoryParam<StationManager, String, void>(
(id, _) => StationManager(id),
);
// 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<
📚 Complete documentation available at flutter-it.dev Check out the comprehensive docs with detailed guides, examples, and best practices! A blazing-fast service locator for Dart and Flutter that makes dependency management simple.

