Advanced
Implementing the Disposable Interface
Instead of passing a disposing function on registration or when pushing a Scope from V7.0 on your objects onDispose() method will be called if the object that you register implements the Disposable interface:
abstract class Disposable {
FutureOr onDispose();
}Find All Instances by Type: findAll<T>()
Find all registered instances that match a given type with powerful filtering and matching options.
// ignore_for_file: missing_function_body, unused_element
List<T> findAll<T>({
bool includeSubtypes = true,
bool inAllScopes = false,
String? onlyInScope,
bool includeMatchedByRegistrationType = true,
bool includeMatchedByInstance = true,
bool instantiateLazySingletons = false,
bool callFactories = false,
}) =>
[];Performance Note
Unlike get_it's O(1) Map-based lookups, findAll() performs an O(n) linear search through all registrations. Use sparingly in performance-critical code. Performance can be improved by limiting the search to a single scope using onlyInScope.
Parameters:
Type Matching:
includeSubtypes- If true (default), matches T and all subtypes; if false, matches only exact type T
Scope Control:
inAllScopes- If true, searches all scopes (default: false, current scope only)onlyInScope- Search only the named scope (takes precedence overinAllScopes)
Matching Strategy:
includeMatchedByRegistrationType- Match by registered type (default: true)includeMatchedByInstance- Match by actual instance type (default: true)
Side Effects:
instantiateLazySingletons- Instantiate lazy singletons that match (default: false)callFactories- Call factories that match to include their instances (default: false)
Example - Basic type matching:
abstract class IOutput {
void write(String message);
}
class FileOutput implements IOutput {
@override
void write(String message) => File('log.txt').writeAsStringSync(message);
}
class ConsoleOutput implements IOutput {
@override
void write(String message) => print(message);
}
// Register different implementation types
void main() {
getIt.registerSingleton<FileOutput>(FileOutput());
getIt.registerLazySingleton<ConsoleOutput>(() => ConsoleOutput());
// Find by interface (registration type matching)
final outputs = getIt.findAll<IOutput>();
print('outputs: $outputs');
// Returns: [FileOutput] only (ConsoleOutput not instantiated yet)
}Example - Include lazy singletons
void main() async {
// Instantiate lazy singletons that match
final all = getIt.findAll<IOutput>(
instantiateLazySingletons: true,
);
// Returns: [FileOutput, ConsoleOutput]
// ConsoleOutput is now created and cached
print('Found: $all');
}Example - Include factories
void main() async {
getIt.registerFactory<IOutput>(() => RemoteOutput('https://api.example.com'));
// Include factories by calling them
final withFactories = getIt.findAll<IOutput>(
instantiateLazySingletons: true,
callFactories: true,
);
// Returns: [FileOutput, ConsoleOutput, RemoteOutput]
// Each factory call creates a new instance
}Example - Exact type matching
class BaseLogger {}
class FileLogger extends BaseLogger {}
class ConsoleLogger extends BaseLogger {}
void main() {
getIt.registerSingleton<BaseLogger>(FileLogger());
getIt.registerSingleton<BaseLogger>(ConsoleLogger());
// Find subtypes (default)
final allLoggers = getIt.findAll<BaseLogger>();
print('allLoggers: $allLoggers');
// Returns: [FileLogger, ConsoleLogger]
// Find exact type only
final exactBase = getIt.findAll<BaseLogger>(
includeSubtypes: false,
);
// Returns: [] (no exact BaseLogger instances, only subtypes)
}Example - Instance vs Registration Type
void main() async {
// Register as FileOutput but it implements IOutput
getIt.registerSingleton<FileOutput>(FileOutput('/path/to/file.txt'));
// Match by registration type
final byRegistration = getIt.findAll<IOutput>(
includeMatchedByRegistrationType: true,
includeMatchedByInstance: false,
);
// Returns: [] (registered as FileOutput, not IOutput)
// Match by instance type
final byInstance = getIt.findAll<IOutput>(
includeMatchedByRegistrationType: false,
includeMatchedByInstance: true,
);
// Returns: [FileOutput] (instance implements IOutput)
}Example - Scope control
void main() {
// Base scope
getIt.registerSingleton<IOutput>(FileOutput('/tmp/output.txt'));
// Push scope
getIt.pushNewScope(scopeName: 'session');
getIt.registerSingleton<IOutput>(ConsoleOutput());
// Current scope only (default)
final current = getIt.findAll<IOutput>();
print('current: $current');
// Returns: [ConsoleOutput]
// All scopes
final all = getIt.findAll<IOutput>(inAllScopes: true);
print('all: $all');
// Returns: [ConsoleOutput, FileOutput]
// Specific scope
final base = getIt.findAll<IOutput>(onlyInScope: 'baseScope');
print('base: $base');
// Returns: [FileOutput]
}Use cases:
- Find all implementations of a plugin interface
- Collect all registered validators/processors
- Runtime dependency graph visualization
- Testing: verify all expected types are registered
- Migration tools: find instances of deprecated types
Validation rules:
includeSubtypes=falserequiresincludeMatchedByInstance=falseinstantiateLazySingletons=truerequiresincludeMatchedByRegistrationType=truecallFactories=truerequiresincludeMatchedByRegistrationType=true
Throws:
StateErrorifonlyInScopedoesn't existArgumentErrorif validation rules are violated
Reference Counting
Reference counting helps manage singleton lifecycle when multiple consumers might need the same instance, especially useful for recursive scenarios like navigation.
The Problem
Imagine a detail page that can be pushed recursively (e.g., viewing related items, navigating through a hierarchy):
Home → DetailPage(item1) → DetailPage(item2) → DetailPage(item3)Without reference counting:
- First DetailPage registers
DetailService - Second DetailPage tries to register → Error or must skip registration
- First DetailPage pops, disposes service → Breaks remaining pages
The Solution: registerSingletonIfAbsent and releaseInstance
T registerSingletonIfAbsent<T>(
T Function() factoryFunc, {
String? instanceName,
DisposingFunc<T>? dispose,
}) =>
factoryFunc();
void releaseInstance(Object instance) {}How it works:
- First call: Creates instance, registers, sets reference count to 1
- Subsequent calls: Returns existing instance, increments counter
releaseInstance: Decrements counter- When counter reaches 0: Unregisters and disposes
Recursive Navigation Example
class DetailService extends ChangeNotifier {
final String itemId;
String? data;
bool isLoading = false;
DetailService(this.itemId) {
// Trigger async loading in constructor (fire and forget)
_loadData();
}
Future<void> _loadData() async {
if (data != null) return; // Already loaded
isLoading = true;
notifyListeners();
print('Loading data for $itemId from backend...');
// Simulate backend call
await Future.delayed(Duration(seconds: 1));
data = 'Data for $itemId';
isLoading = false;
notifyListeners();
}
}
class DetailPage extends WatchingWidget {
final String itemId;
const DetailPage(this.itemId);
@override
Widget build(BuildContext context) {
// Register once when widget is created, dispose when widget is disposed
callOnce(
(context) {
// Register or get existing - increments reference count
getIt.registerSingletonIfAbsent<DetailService>(
() => DetailService(itemId),
instanceName: itemId,
);
},
dispose: () {
// Decrements reference count when widget disposes
getIt.releaseInstance(getIt<DetailService>(instanceName: itemId));
},
);
// Watch the service - rebuilds when notifyListeners() called
final service = watchIt<DetailService>(instanceName: itemId);
return Scaffold(
appBar: AppBar(title: Text('Detail $itemId')),
body: service.isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Text(service.data ?? 'No data'),
ElevatedButton(
onPressed: () {
// Can push same page recursively
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage('related-$itemId'),
),
);
},
child: const Text('View Related'),
),
],
),
);
}
}Flow:
Push DetailPage(item1) → Create service, load data, refCount = 1
Push DetailPage(item2) → Create service, load data, refCount = 1
Push DetailPage(item1) → Get existing (NO reload!), refCount = 2
Pop DetailPage(item1) → Release, refCount = 1 (service stays)
Pop DetailPage(item2) → Release, refCount = 0 (service disposed)
Pop DetailPage(item1) → Release, refCount = 0 (service disposed)Benefits:
- ✅ Service created synchronously (no async factory needed)
- ✅ Async loading triggered in constructor
- ✅ No duplicate loading for same item (checked before loading)
- ✅ Automatic memory management via reference counting
- ✅ Reactive UI updates via `watch_it` (rebuilds on state changes)
- ✅ ChangeNotifier automatically disposed when refCount reaches 0
- ✅ Each itemId uniquely identified via `instanceName`
Key Integration: This example demonstrates how get_it (reference counting) and watch_it (reactive UI) work together seamlessly for complex navigation patterns.
Force Release: ignoreReferenceCount
In rare cases, you might need to force unregister regardless of reference count:
// Force unregister even if refCount > 0
getIt.unregister<MyService>(ignoreReferenceCount: true);Use with Caution
Only use ignoreReferenceCount: true when you're certain no other code is using the instance. This can cause crashes if other parts of your app still hold references.
When to Use Reference Counting
✅ Good use cases:
- Recursive navigation (same page pushed multiple times)
- Services needed by multiple simultaneously active features
- Complex hierarchical component structures
❌️ Don't use when:
- Simple singleton that lives for app lifetime (use regular
registerSingleton) - One-to-one widget-service relationship (use scopes)
- Testing (use scopes to shadow instead)
Best Practices
- Always pair register with release: Every
registerSingletonIfAbsentshould have a matchingreleaseInstance - Store instance reference: Keep the returned instance so you can release the correct one
- Release in dispose/cleanup: Tie release to widget/component lifecycle
- Document shared resources: Make it clear when a service uses reference counting
Utility Methods
Safe Retrieval: maybeGet<T>()
Returns null instead of throwing an exception if the type is not registered. Useful for optional dependencies and feature flags.
/// like [get] but returns null if the instance is not found
T? maybeGet<T extends Object>({
dynamic param1,
dynamic param2,
String? instanceName,
Type? type,
});Example:
// Feature flag scenario
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
final logger = getIt.maybeGet<Logger>();
print('logger: $logger');
logger?.log('Building MyWidget'); // Safe even if Logger not registered
return const Text('Hello');
}
}
// Graceful degradation
Widget getUIForPremiumStatus() {
final premiumFeature = getIt.maybeGet<PremiumFeature>();
print('premiumFeature: $premiumFeature');
if (premiumFeature != null) {
return PremiumUI(feature: premiumFeature);
} else {
return const BasicUI(); // Fallback when premium not available
}
}
void main() {
final analyticsService = getIt.maybeGet<AnalyticsService>();
print('analyticsService: $analyticsService');
if (analyticsService != null) {
analyticsService.trackEvent('user_action');
}
final ui = getUIForPremiumStatus();
print('UI: $ui');
}When to use:
- ✅ Optional features that may or may not be registered
- ✅ Feature flags (service registered only when feature enabled)
- ✅ Platform-specific services (might not exist on all platforms)
- ✅ Graceful degradation scenarios
Don't use when:
- ❌️ The dependency is required - use
get<T>()to fail fast - ❌️ Missing registration indicates a bug - exception is helpful
Instance Renaming: changeTypeInstanceName()
Rename a registered instance without unregistering and re-registering (avoids triggering dispose functions).
// ignore_for_file: missing_function_body, unused_element
void changeTypeInstanceName<T>({
String? instanceName,
required String newInstanceName,
T? instance,
}) {}Example:
class UserModel extends ChangeNotifier {
String username;
String email;
UserModel(this.username, this.email);
Future<void> updateUsername(String newUsername) async {
// Update on backend via API (stubbed)
final oldUsername = username;
username = newUsername;
// Rename the instance in GetIt to match new username
getIt.changeTypeInstanceName<UserModel>(
instanceName: oldUsername,
newInstanceName: newUsername,
);
notifyListeners();
}
}Use cases:
- User profile updates where username is the instance identifier
- Dynamic entity names that can change at runtime
- Avoiding disposal side effects from unregister/register cycle
- Maintaining instance state while updating its identifier
Avoids Dispose
Unlike unregister() + register(), this doesn't trigger dispose functions, preserving the instance's state.
Lazy Singleton Introspection: checkLazySingletonInstanceExists()
Check if a lazy singleton has been instantiated yet (without triggering its creation).
// ignore_for_file: missing_function_body, unused_element
import 'package:get_it/get_it.dart';
bool checkLazySingletonInstanceExists<T>({
String? instanceName,
}) =>
false;Example:
// Register lazy singleton
getIt.registerLazySingleton<HeavyService>(() => HeavyService());
// Check if it's been created yet
if (getIt.checkLazySingletonInstanceExists<HeavyService>()) {
print('HeavyService already created');
} else {
print('HeavyService not created yet - will be lazy loaded');
}
// Access triggers creation
final service = getIt<HeavyService>();
print('service: $service');
// Now it exists
assert(getIt.checkLazySingletonInstanceExists<HeavyService>() == true);Use cases:
- Performance monitoring (track which services have been initialized)
- Conditional initialization (pre-warm services if not created)
- Testing lazy loading behavior
- Debugging initialization order issues
Example - Pre-warming:
void preWarmCriticalServices() {
// Only initialize if not already created
if (!getIt.checkLazySingletonInstanceExists<DatabaseService>()) {
getIt<DatabaseService>(); // Trigger creation
}
if (!getIt.checkLazySingletonInstanceExists<CacheService>()) {
getIt<CacheService>(); // Trigger creation
}
}Reset All Lazy Singletons: resetLazySingletons()
Reset all instantiated lazy singletons at once. This clears their instances so they'll be recreated on next access.
Future<void> resetLazySingletons({
bool dispose = true,
bool inAllScopes = false,
String? onlyInScope,
}) async {}Parameters:
dispose- If true (default), calls dispose functions before resettinginAllScopes- If true, resets lazy singletons across all scopesonlyInScope- Reset only in the named scope (takes precedence overinAllScopes)
Example - Basic usage:
void main() async {
// Register lazy singletons
getIt.registerLazySingleton<CacheService>(() => CacheService());
getIt.registerLazySingleton<UserPreferences>(() => UserPreferences());
// Access them (creates instances)
final cache = getIt<CacheService>();
print('cache: $cache');
final prefs = getIt<UserPreferences>();
print('prefs: $prefs');
// Reset all lazy singletons in current scope
await getIt.resetLazySingletons();
// Next access creates fresh instances
final newCache = getIt<CacheService>();
print('newCache: $newCache'); // New instance
}Example - With scopes:
void main() async {
// Base scope lazy singletons
getIt.registerLazySingleton<GlobalCache>(() => GlobalCache());
// Push scope and register more
getIt.pushNewScope(scopeName: 'session');
getIt.registerLazySingleton<SessionCache>(() => SessionCache());
getIt.registerLazySingleton<UserState>(() => UserState());
// Access them
final globalCache = getIt<GlobalCache>();
print('globalCache: $globalCache');
final sessionCache = getIt<SessionCache>();
print('sessionCache: $sessionCache');
// Reset only current scope ('session')
await getIt.resetLazySingletons();
// GlobalCache NOT reset, SessionCache and UserState ARE reset
// Reset all scopes
await getIt.resetLazySingletons(inAllScopes: true);
// Both GlobalCache and SessionCache are reset
// Reset only specific scope
await getIt.resetLazySingletons(onlyInScope: 'baseScope');
// Only GlobalCache is reset
}Use cases:
- State reset between tests
- User logout (clear session-specific lazy singletons)
- Memory optimization (reset caches that can be recreated)
- Scope-specific cleanup without popping the scope
Behavior:
- Only resets lazy singletons that have been instantiated
- Uninstantiated lazy singletons are not affected
- Regular singletons and factories are not affected
- Supports both sync and async dispose functions
Advanced Introspection: findFirstObjectRegistration<T>()
Get metadata about a registration without retrieving the instance.
/// find the first registration that matches the type [T]/[instanceName] or the [instance]
ObjectRegistration? findFirstObjectRegistration<T extends Object>({
Object? instance,
String? instanceName,
});Example:
final registration = getIt.findFirstObjectRegistration<MyService>();
print('registration: $registration');
if (registration != null) {
print(
'Type: ${registration.registrationType}'); // factory, singleton, lazy, etc.
print('Instance name: ${registration.instanceName}');
print('Is async: ${registration.isAsync}');
print('Is ready: ${registration.isReady}');
}Use cases:
- Building tools/debugging utilities on top of GetIt
- Runtime dependency graph visualization
- Advanced lifecycle management
- Debugging registration issues
Accessing an object inside GetIt by a runtime type
In rare occasions you might be faced with the problem that you don't know the type that you want to retrieve from GetIt at compile time which means you can't pass it as a generic parameter. For this the get functions have an optional type parameter
getIt.registerSingleton(TestClass());
final instance1 = getIt.get(type: TestClass);
print('instance1: $instance1');
expect(instance1 is TestClass, true);Be careful that the receiving variable has the correct type and don't pass type and a generic parameter.
More than one instance of GetIt
While not recommended, you can create your own independent instance of GetIt if you don't want to share your locator with some other package or because the physics of your planet demands it 😃
/// To make sure you really know what you are doing
/// you have to first enable this feature:
GetIt myOwnInstance = GetIt.asNewInstance();This new instance does not share any registrations with the singleton instance.