diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a57626..d794646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ -## [8.0.0] - 07.05.2024 -* `getAll()` and `getAllAsync()` not have a `fromAllScopes` parameter. +## [8.0.0-pre-1] - 07.05.2024 +* `getAll()` and `getAllAsync()` now have a `fromAllScopes` parameter. * adding safeguards according to https://github.com/fluttercommunity/get_it/issues/364 to make it impossilble to call `push/popScope` while the `init()` function of another pushScope is running. * fixed an usafe type check when using a runtime type to acess an object in get_it. ## [7.7.0] - 15.04.2024 thanks to the PR by @kasefuchs https://github.com/fluttercommunity/get_it/pull/361 `getAll` is now available in an async version too. diff --git a/lib/get_it.dart b/lib/get_it.dart index b35309e..ff59ea8 100644 --- a/lib/get_it.dart +++ b/lib/get_it.dart @@ -309,6 +309,34 @@ abstract class GetIt { DisposingFunc? dispose, }); + /// ---- With reference counting ---- + /// + /// [registerSingletonIfAbsent] and [releaseInstance] are used to manage the lifecycle of + /// a Singleton. This is useful if you register an object when you push a Page and this page can get + /// pused recursively. In that case you don't want to dispose the object when first of these pages is popped + /// + + /// Only registers a type new as Singleton if it is not already registered. Otherwise it returns + /// the existing instance and increments an internal reference counter to ensure that matching + /// [unregister] or [releaseInstance] calls will decrement the reference counter an won't unregister + /// and dispose the registration as long as the reference counter is > 0. + /// [T] type/interface that is used for the registration and the access via [get] + /// [factoryFunc] that is callled to create the instance if it is not already registered + /// [instanceName] optional key to register more than one instance of one type + /// [dispose] disposing function that is autmatically called before the object is removed from get_it + T registerSingletonIfAbsent( + T Function() factoryFunc, { + String? instanceName, + DisposingFunc? dispose, + }); + + /// checks if a regiserter Singleton has an reference counter > 0 + /// if so it decrements the reference counter and if it reaches 0 it + /// unregisters the Singleton + /// if called on an object that's reference counter was never incremented + /// it will immediately unregister and dispose the object + void releaseInstance(Object instance); + /// registers a type as Singleton by passing a factory function of that type /// that will be called when all dependent Singletons are ready /// [T] type to register @@ -494,10 +522,15 @@ abstract class GetIt { /// [instanceName] if you need to dispose any resources you can do it using /// [disposingFunction] function that provides an instance of your class to be disposed. /// This function overrides the disposing you might have provided when registering. + /// If you have enabled referece counting when registering, [unregister] will only unregister and dispose the object + /// if referenceCount is 0 + /// [ignoreReferenceCount] if `true` it will ignore the reference count and unregister the object + /// only use this if you know what you are doing FutureOr unregister({ Object? instance, String? instanceName, FutureOr Function(T)? disposingFunction, + bool ignoreReferenceCount = false, }); /// returns a Future that completes if all asynchronously created Singletons and any diff --git a/lib/get_it_impl.dart b/lib/get_it_impl.dart index 11a873f..e762337 100644 --- a/lib/get_it_impl.dart +++ b/lib/get_it_impl.dart @@ -95,6 +95,8 @@ class _ServiceFactory { final bool shouldSignalReady; + int _refenceCount = 0; + _ServiceFactory( this._getItInstance, this.factoryType, { @@ -810,6 +812,60 @@ class _GetItImplementation implements GetIt { return instance; } + /// Only registers a type new as Singleton if it is not already registered. Otherwise it returns + /// the existing instance and increments an internal reference counter to ensure that matching + /// [unregister] or [releaseInstance] calls will decrement the reference counter an won't unregister + /// and dispose the registration as long as the reference counter is > 0. + /// [T] type/interface that is used for the registration and the access via [get] + /// [factoryFunc] that is callled to create the instance if it is not already registered + /// [instanceName] optional key to register more than one instance of one type + /// [dispose] disposing function that is autmatically called before the object is removed from get_it + @override + T registerSingletonIfAbsent( + T Function() factoryFunc, { + String? instanceName, + DisposingFunc? dispose, + }) { + final existingFactory = + _findFirstFactoryByNameAndTypeOrNull(instanceName); + if (existingFactory != null) { + throwIfNot( + existingFactory.factoryType == _ServiceFactoryType.constant && + !existingFactory.isAsync, + StateError( + 'registerSingletonIfAbsent can only be called for a type that is already registered as Singleton and not for factories or async/lazy Singletons')); + existingFactory._refenceCount++; + return existingFactory.instance!; + } + + final instance = factoryFunc(); + _register( + type: _ServiceFactoryType.constant, + instance: instance, + instanceName: instanceName, + isAsync: false, + shouldSignalReady: false, + disposeFunc: dispose, + ); + return instance; + } + + /// checks if a regiserter Singleton has an reference counter > 0 + /// if so it decrements the reference counter and if it reaches 0 it + /// unregisters the Singleton + /// if called on an object that's reference counter was never incremented + /// it will immediately unregister and dispose the object + @override + void releaseInstance(Object instance) { + final registerdFactory = _findFirstFactoryByInstanceOrNull(instance); + if (registerdFactory != null) { + if (registerdFactory._refenceCount < 1) { + unregister(instance: instance); + } + registerdFactory._refenceCount--; + } + } + /// registers a type as Singleton by passing an factory function of that type /// that will be called on each call of [get] on that type /// [T] type to register @@ -908,6 +964,145 @@ class _GetItImplementation implements GetIt { ); } + /// Tests if an [instance] of an object or aType [T] or a name [instanceName] + /// is registered inside GetIt + @override + bool isRegistered({ + Object? instance, + String? instanceName, + }) { + if (instance != null) { + return _findFirstFactoryByInstanceOrNull(instance) != null; + } else { + return _findFirstFactoryByNameAndTypeOrNull(instanceName) != null; + } + } + + /// Unregister an instance of an object or a factory/singleton by Type [T] or by name [instanceName] + /// if you need to dispose any resources you can pass in a [disposingFunction] function + /// that provides an instance of your class to be disposed + /// If you have provided an disposing functin when you registered the object that one will be called automatically + /// If you have enabled referece counting when registering, [unregister] will only unregister and dispose the object + /// if referenceCount is 0 + /// + @override + FutureOr unregister({ + Object? instance, + String? instanceName, + FutureOr Function(T)? disposingFunction, + bool ignoreReferenceCount = false, + }) async { + final factoryToRemove = instance != null + ? _findFactoryByInstance(instance) + : _findFactoryByNameAndType(instanceName); + + throwIf( + factoryToRemove.objectsWaiting.isNotEmpty, + StateError( + 'There are still other objects waiting for this instance so signal ready', + ), + ); + + if (factoryToRemove._refenceCount > 0) { + factoryToRemove._refenceCount--; + return; + } + final typeRegistration = factoryToRemove.registeredIn; + + if (instanceName != null) { + typeRegistration.namedFactories.remove(instanceName); + } else { + typeRegistration.factories.remove(factoryToRemove); + } + if (typeRegistration.isEmpty) { + factoryToRemove.registrationScope.typeRegistrations.remove(T); + } + + if (factoryToRemove.instance != null) { + if (disposingFunction != null) { + final dispose = disposingFunction.call(factoryToRemove.instance! as T); + if (dispose is Future) { + await dispose; + } + } else { + final dispose = factoryToRemove.dispose(); + if (dispose is Future) { + await dispose; + } + } + } + } + + /// Clears the instance of a lazy singleton, + /// being able to call the factory function on the next call + /// of [get] on that type again. + /// you select the lazy Singleton you want to reset by either providing + /// an [instance], its registered type [T] or its registration name. + /// if you need to dispose some resources before the reset, you can + /// provide a [disposingFunction] + @override + FutureOr resetLazySingleton({ + T? instance, + String? instanceName, + FutureOr Function(T)? disposingFunction, + }) async { + _ServiceFactory instanceFactory; + + if (instance != null) { + instanceFactory = _findFactoryByInstance(instance); + } else { + instanceFactory = _findFactoryByNameAndType(instanceName); + } + throwIfNot( + instanceFactory.factoryType == _ServiceFactoryType.lazy, + StateError( + 'There is no type ${instance.runtimeType} registered as LazySingleton in GetIt', + ), + ); + + dynamic disposeReturn; + if (instanceFactory.instance != null) { + if (disposingFunction != null) { + disposeReturn = disposingFunction.call(instanceFactory.instance! as T); + } else { + disposeReturn = instanceFactory.dispose(); + } + } + + instanceFactory.instance = null; + instanceFactory.pendingResult = null; + instanceFactory._readyCompleter = Completer(); + if (disposeReturn is Future) { + await disposeReturn; + } + } + + List<_ServiceFactory> get _allFactories => + _scopes.fold>( + [], + (sum, x) => sum..addAll(x.allFactories), + ); + + _ServiceFactory? _findFirstFactoryByInstanceOrNull(Object instance) { + final registeredFactories = + _allFactories.where((x) => identical(x.instance, instance)); + return registeredFactories.isEmpty ? null : registeredFactories.first; + } + + _ServiceFactory _findFactoryByInstance(Object instance) { + final registeredFactory = _findFirstFactoryByInstanceOrNull(instance); + + throwIf( + registeredFactory == null, + StateError( + 'This instance of the type ${instance.runtimeType} is not available in GetIt ' + 'If you have registered it as LazySingleton, are you sure you have used ' + 'it at least once?'), + ); + + return registeredFactory!; + } + /// Clears all registered types. Handy when writing unit tests. @override Future reset({bool dispose = true}) async { @@ -1133,6 +1328,7 @@ class _GetItImplementation implements GetIt { _Scope registrationScope; int i = _scopes.length; + // find the first not final scope do { i--; registrationScope = _scopes[i]; @@ -1143,7 +1339,7 @@ class _GetItImplementation implements GetIt { ); final existingTypeRegistration = registrationScope.typeRegistrations[T]; - // if we already a registration for this type we have to check if its a valid re-registration + // if we already have a registration for this type we have to check if its a valid re-registration if (existingTypeRegistration != null) { if (instanceName != null) { throwIf( @@ -1362,136 +1558,6 @@ class _GetItImplementation implements GetIt { } } - /// Tests if an [instance] of an object or aType [T] or a name [instanceName] - /// is registered inside GetIt - @override - bool isRegistered({ - Object? instance, - String? instanceName, - }) { - if (instance != null) { - return _findFirstFactoryByInstanceOrNull(instance) != null; - } else { - return _findFirstFactoryByNameAndTypeOrNull(instanceName) != null; - } - } - - /// Unregister an instance of an object or a factory/singleton by Type [T] or by name [instanceName] - /// if you need to dispose any resources you can do it using [disposingFunction] function - /// that provides an instance of your class to be disposed - @override - FutureOr unregister({ - Object? instance, - String? instanceName, - FutureOr Function(T)? disposingFunction, - }) async { - final factoryToRemove = instance != null - ? _findFactoryByInstance(instance) - : _findFactoryByNameAndType(instanceName); - - throwIf( - factoryToRemove.objectsWaiting.isNotEmpty, - StateError( - 'There are still other objects waiting for this instance so signal ready', - ), - ); - - final typeRegistration = factoryToRemove.registeredIn; - - if (instanceName != null) { - typeRegistration.namedFactories.remove(instanceName); - } else { - typeRegistration.factories.remove(factoryToRemove); - } - if (typeRegistration.isEmpty) { - factoryToRemove.registrationScope.typeRegistrations.remove(T); - } - - if (factoryToRemove.instance != null) { - if (disposingFunction != null) { - final dispose = disposingFunction.call(factoryToRemove.instance! as T); - if (dispose is Future) { - await dispose; - } - } else { - final dispose = factoryToRemove.dispose(); - if (dispose is Future) { - await dispose; - } - } - } - } - - /// Clears the instance of a lazy singleton, - /// being able to call the factory function on the next call - /// of [get] on that type again. - /// you select the lazy Singleton you want to reset by either providing - /// an [instance], its registered type [T] or its registration name. - /// if you need to dispose some resources before the reset, you can - /// provide a [disposingFunction] - @override - FutureOr resetLazySingleton({ - T? instance, - String? instanceName, - FutureOr Function(T)? disposingFunction, - }) async { - _ServiceFactory instanceFactory; - - if (instance != null) { - instanceFactory = _findFactoryByInstance(instance); - } else { - instanceFactory = _findFactoryByNameAndType(instanceName); - } - throwIfNot( - instanceFactory.factoryType == _ServiceFactoryType.lazy, - StateError( - 'There is no type ${instance.runtimeType} registered as LazySingleton in GetIt', - ), - ); - - dynamic disposeReturn; - if (instanceFactory.instance != null) { - if (disposingFunction != null) { - disposeReturn = disposingFunction.call(instanceFactory.instance! as T); - } else { - disposeReturn = instanceFactory.dispose(); - } - } - - instanceFactory.instance = null; - instanceFactory.pendingResult = null; - instanceFactory._readyCompleter = Completer(); - if (disposeReturn is Future) { - await disposeReturn; - } - } - - List<_ServiceFactory> get _allFactories => - _scopes.fold>( - [], - (sum, x) => sum..addAll(x.allFactories), - ); - - _ServiceFactory? _findFirstFactoryByInstanceOrNull(Object instance) { - final registeredFactories = - _allFactories.where((x) => identical(x.instance, instance)); - return registeredFactories.isEmpty ? null : registeredFactories.first; - } - - _ServiceFactory _findFactoryByInstance(Object instance) { - final registeredFactory = _findFirstFactoryByInstanceOrNull(instance); - - throwIf( - registeredFactory == null, - StateError( - 'This instance of the type ${instance.runtimeType} is not available in GetIt ' - 'If you have registered it as LazySingleton, are you sure you have used ' - 'it at least once?'), - ); - - return registeredFactory!; - } - /// Used to manually signal the ready state of a Singleton. /// If you want to use this mechanism you have to pass [signalsReady==true] when registering /// the Singleton. diff --git a/pubspec.yaml b/pubspec.yaml index a360dc9..d1c7963 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: get_it description: Simple direct Service Locator that allows to decouple the interface from a concrete implementation and to access the concrete implementation from everywhere in your App" -version: 8.0.0 +version: 8.0.0-pre-1 maintainer: Thomas Burkhart (@escamoteur) homepage: https://github.com/fluttercommunity/get_it funding: diff --git a/test/get_it_test.dart b/test/get_it_test.dart index 6554f6d..852f8b5 100644 --- a/test/get_it_test.dart +++ b/test/get_it_test.dart @@ -797,6 +797,82 @@ void main() { throwsA(const TypeMatcher()), ); }); + test('testing reference counting', () async { + final getIt = GetIt.instance; + disposeCounter = 0; + constructorCounter = 0; + + getIt.registerSingletonIfAbsent( + () => TestClass(), + dispose: (param) => disposeCounter++, + ); + + final instance1 = getIt.get(); + + expect(instance1 is TestClass, true); + + final instance2 = getIt.registerSingletonIfAbsent(() { + assert(false, 'This should not be called'); + return TestClass(); + }, dispose: (param) { + assert(false, 'This should not be called'); + }); + + expect(instance1, instance2); + + expect(constructorCounter, 1); + + getIt.releaseInstance(instance2); + + expect(getIt.isRegistered(), true); + + getIt.releaseInstance(instance2); + + expect(disposeCounter, 1); + + expect( + () => getIt.get(), + throwsA(const TypeMatcher()), + ); + }); + test('testing reference counting - unregister', () async { + final getIt = GetIt.instance; + disposeCounter = 0; + constructorCounter = 0; + + getIt.registerSingletonIfAbsent( + () => TestClass(), + dispose: (param) => disposeCounter++, + ); + + final instance1 = getIt.get(); + + expect(instance1 is TestClass, true); + + final instance2 = getIt.registerSingletonIfAbsent(() { + assert(false, 'This should not be called'); + return TestClass(); + }, dispose: (param) { + assert(false, 'This should not be called'); + }); + + expect(instance1, instance2); + + expect(constructorCounter, 1); + + getIt.unregister(); + + expect(getIt.isRegistered(), true); + + getIt.unregister(); + + expect(disposeCounter, 1); + + expect( + () => getIt.get(), + throwsA(const TypeMatcher()), + ); + }); test('unregister by name', () async { final getIt = GetIt.instance;