Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ Choose the lifetime that fits your needs:
- **Factory** — New instance every time. Great for stateless services or objects with short lifetimes.
[Read more →](https://flutter-it.dev/documentation/get_it/object_registration#factory)

> **Avoid untyped tear-offs as factories.** Registering `getIt.call` (or an untyped `getIt.get` tear-off) as `registerLazySingleton<Iface>(...)` re-enters `get<Iface>()` and used to overflow the stack. GetIt now throws a clear `StateError` for that circular self-resolution. Prefer `() => getIt<Impl>()` or a named function that keeps the concrete type argument.


### Advanced Features

- **Scopes** — Create hierarchical registration scopes for different app states (login/logout, sessions, feature flags).
Expand Down
237 changes: 157 additions & 80 deletions lib/get_it_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ class _ObjectRegistration<T extends Object, P1, P2>
/// they are stored here
final List<Type> objectsWaiting = [];

/// True while [creationFunction] / async factory for this registration is
/// running. Used to fail fast on circular self-resolution (e.g. registering
/// `getIt.call` as a factory for `T`, which re-enters `get<T>()`).
bool _creationInProgress = false;

@override
bool get isReady => _readyCompleter.isCompleted;

Expand Down Expand Up @@ -213,6 +218,47 @@ class _ObjectRegistration<T extends Object, P1, P2>
}
}

Never _throwCircularSelfResolution() {
final named =
instanceName != null ? ' (instanceName: "$instanceName")' : '';
throw StateError(
'GetIt: Circular dependency detected while creating $registeredWithType$named.\n'
'This often happens when registering with an untyped tear-off of get/call, e.g.:\n'
' getIt.registerLazySingleton<Iface>(getIt.call);\n'
'which re-enters get<$registeredWithType>() and recurses until StackOverflowError.\n'
'Use an explicit factory that constructs or resolves a concrete type, e.g.:\n'
' getIt.registerLazySingleton<Iface>(() => getIt<Impl>());\n'
'or a named function tear-off that keeps the concrete type argument.',
);
}

T _runCreation(T Function() create) {
if (_creationInProgress) {
_throwCircularSelfResolution();
}
_creationInProgress = true;
try {
return create();
} finally {
_creationInProgress = false;
}
}

/// Guards only the synchronous call that starts creation. Cleared when the
/// factory returns a [Future], not when that Future completes — so overlapping
/// [getAsync] on always-new / cached factories stay valid.
Future<T> _runCreationAsync(Future<T> Function() create) {
if (_creationInProgress) {
_throwCircularSelfResolution();
}
_creationInProgress = true;
try {
return create();
} finally {
_creationInProgress = false;
}
}

/// returns an instance depending on the type of the registration if [async==false]
T getObject(dynamic param1, dynamic param2) {
assert(
Expand All @@ -230,42 +276,48 @@ class _ObjectRegistration<T extends Object, P1, P2>
if (creationFunctionParam != null) {
// Validate parameters in debug mode
_validateFactoryParams(param1, param2);
return creationFunctionParam!(param1 as P1, param2 as P2);
return _runCreation(
() => creationFunctionParam!(param1 as P1, param2 as P2),
);
} else {
return creationFunction!();
return _runCreation(creationFunction!);
}
case ObjectRegistrationType.cachedFactory:
if (weakReferenceInstance?.target != null &&
param1 == lastParam1 &&
param2 == lastParam2) {
return weakReferenceInstance!.target!;
} else {
T newInstance;
if (creationFunctionParam != null) {
// Validate parameters in debug mode BEFORE casting
_validateFactoryParams(param1, param2);
lastParam1 = param1 as P1?;
lastParam2 = param2 as P2?;
newInstance = creationFunctionParam!(param1 as P1, param2 as P2);
} else {
newInstance = creationFunction!();
}
final T newInstance = _runCreation(() {
if (creationFunctionParam != null) {
// Validate parameters in debug mode BEFORE casting
_validateFactoryParams(param1, param2);
lastParam1 = param1 as P1?;
lastParam2 = param2 as P2?;
return creationFunctionParam!(param1 as P1, param2 as P2);
} else {
return creationFunction!();
}
});
weakReferenceInstance = WeakReference(newInstance);
return newInstance;
}
case ObjectRegistrationType.constant:
return instance!;
case ObjectRegistrationType.lazy:
if (instance == null) {
if (useWeakReference) {
if (weakReferenceInstance != null) {
/// this means that the instance was already created and disposed
_readyCompleter = Completer();
_runCreation(() {
if (useWeakReference) {
if (weakReferenceInstance != null) {
/// this means that the instance was already created and disposed
_readyCompleter = Completer();
}
weakReferenceInstance = WeakReference(creationFunction!());
} else {
_instance = creationFunction!();
}
weakReferenceInstance = WeakReference(creationFunction!());
} else {
_instance = creationFunction!();
}
return instance!;
});
objectsWaiting.clear();
_readyCompleter.complete();

Expand All @@ -290,8 +342,14 @@ class _ObjectRegistration<T extends Object, P1, P2>
return instance!;
}
} catch (e, s) {
_debugOutput('Error while creating $T');
_debugOutput('Stack trace:\n $s');
// Only build/print the stack when debug output is enabled. Eagerly
// interpolating `$s` can amplify StackOverflowError into native crashes.
if (_isDebugMode && !GetIt.noDebugOutput) {
// ignore: avoid_print
print('Error while creating $T');
// ignore: avoid_print
print('Stack trace:\n $s');
}
rethrow;
}
}
Expand Down Expand Up @@ -321,35 +379,38 @@ class _ObjectRegistration<T extends Object, P1, P2>
if (asyncCreationFunctionParam != null) {
// Validate parameters in debug mode
_validateFactoryParams(param1, param2);
return asyncCreationFunctionParam!(param1 as P1, param2 as P2)
as Future<R>;
return _runCreationAsync(
() => asyncCreationFunctionParam!(param1 as P1, param2 as P2),
) as Future<R>;
} else {
return asyncCreationFunction!() as Future<R>;
return _runCreationAsync(asyncCreationFunction!) as Future<R>;
}
case ObjectRegistrationType.cachedFactory:
if (weakReferenceInstance?.target != null &&
param1 == lastParam1 &&
param2 == lastParam2) {
return Future<R>.value(weakReferenceInstance!.target! as R);
} else {
if (asyncCreationFunctionParam != null) {
// Validate parameters in debug mode BEFORE casting
_validateFactoryParams(param1, param2);
lastParam1 = param1 as P1?;
lastParam2 = param2 as P2?;
return asyncCreationFunctionParam!(
param1 as P1,
param2 as P2,
).then((value) {
weakReferenceInstance = WeakReference(value);
return value;
}) as Future<R>;
} else {
return asyncCreationFunction!().then((value) {
weakReferenceInstance = WeakReference(value);
return value;
}) as Future<R>;
}
return _runCreationAsync(() {
if (asyncCreationFunctionParam != null) {
// Validate parameters in debug mode BEFORE casting
_validateFactoryParams(param1, param2);
lastParam1 = param1 as P1?;
lastParam2 = param2 as P2?;
return asyncCreationFunctionParam!(
param1 as P1,
param2 as P2,
).then((value) {
weakReferenceInstance = WeakReference(value);
return value;
});
} else {
return asyncCreationFunction!().then((value) {
weakReferenceInstance = WeakReference(value);
return value;
});
}
}) as Future<R>;
}
case ObjectRegistrationType.constant:
if (instance != null) {
Expand All @@ -369,47 +430,63 @@ class _ObjectRegistration<T extends Object, P1, P2>
return pendingResult! as Future<R>;
}

/// Seems this is really the first access to this async Singleton
final asyncResult = asyncCreationFunction!();

pendingResult = asyncResult.then((newInstance) {
if (!shouldSignalReady) {
/// only complete automatically if the registration wasn't marked with
/// [signalsReady==true]
_readyCompleter.complete();
objectsWaiting.clear();
}
if (useWeakReference) {
weakReferenceInstance = WeakReference(newInstance);
} else {
_instance = newInstance;
}
if (_creationInProgress) {
_throwCircularSelfResolution();
}
_creationInProgress = true;

// Call onCreated callback if provided
onCreatedCallback?.call(newInstance);

/// check if we are shadowing an existing Object
final registrationThatWouldbeShadowed =
_getItInstance._findFirstRegistrationByNameAndTypeOrNull(
instanceName,
type: T,
lookInScopeBelow: true,
);

final objectThatWouldbeShadowed =
registrationThatWouldbeShadowed?.instance;
if (objectThatWouldbeShadowed != null &&
objectThatWouldbeShadowed is ShadowChangeHandlers) {
objectThatWouldbeShadowed.onGetShadowed(instance!);
}
return newInstance;
});
return pendingResult! as Future<R>;
/// Seems this is really the first access to this async Singleton
try {
final asyncResult = asyncCreationFunction!();

pendingResult = asyncResult.then((newInstance) {
if (!shouldSignalReady) {
/// only complete automatically if the registration wasn't marked with
/// [signalsReady==true]
_readyCompleter.complete();
objectsWaiting.clear();
}
if (useWeakReference) {
weakReferenceInstance = WeakReference(newInstance);
} else {
_instance = newInstance;
}

// Call onCreated callback if provided
onCreatedCallback?.call(newInstance);

/// check if we are shadowing an existing Object
final registrationThatWouldbeShadowed =
_getItInstance._findFirstRegistrationByNameAndTypeOrNull(
instanceName,
type: T,
lookInScopeBelow: true,
);

final objectThatWouldbeShadowed =
registrationThatWouldbeShadowed?.instance;
if (objectThatWouldbeShadowed != null &&
objectThatWouldbeShadowed is ShadowChangeHandlers) {
objectThatWouldbeShadowed.onGetShadowed(instance!);
}
return newInstance;
}).whenComplete(() {
_creationInProgress = false;
});
return pendingResult! as Future<R>;
} catch (_) {
_creationInProgress = false;
rethrow;
}
}
}
} catch (e, s) {
_debugOutput('Error while creating $T}');
_debugOutput('Stack trace:\n $s');
if (_isDebugMode && !GetIt.noDebugOutput) {
// ignore: avoid_print
print('Error while creating $T');
// ignore: avoid_print
print('Stack trace:\n $s');
}
rethrow;
}
}
Expand Down
Loading