Skip to main content

flutter-it-get-it

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.

来源信息

仓库
flutter-it/flutter_it
最近来源活动
2026年9月19日 20:53
检测到的 SKILL.md 语言
英语
星标
20
分支
1

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
flutter-it-get-it
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 ```dart 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: ```dart 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 ```dart 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 ```dart // 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 ```dart // 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): ```dart // 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 ```dart getIt.isRegistered<ApiClient>(); // bool getIt.unregister<ApiClient>(); // remove registration getIt.skipUnregisterIfNotRegistered = true; // global: unregister of a missing registration is a no-op instead of throwing (v9.3.0+, default false; mirrors skipDoubleRegistration) 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 ``` `findFirstObjectRegistration<T>()` returns an `ObjectRegistration` with `registrationType`, `isAsync`, `acceptsParams` (v9.3.0+: true for `registerFactoryParam`/`registerCachedFactoryParam` and async variants). Passing `param1`/`param2` to a registration with `acceptsParams == false` fails an assert in debug. ## Anti-Patterns ```dart // ❌ 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 ```dart // 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): ```dart 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: ```dart // ❌ 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); } } ```
在 GitHub 查看