Last active
March 18, 2021 04:55
-
-
Save CassiusPacheco/f0eea34e4136083860e8621aad217501 to your computer and use it in GitHub Desktop.
A dependency resolver container for Dart/Flutter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:logging/logging.dart'; | |
typedef InstanceBuilderCallback<S> = S Function(ServiceLocator); | |
typedef InstanceBuilderCallback1<S, A> = S Function(ServiceLocator, A); | |
/// A service lookup class which allows factories of instances and singletons | |
/// to be registered and resolved as part of a dependency injection system. | |
class ServiceLocator { | |
final Map<String, InstanceBuilderCallback> _factories = {}; | |
final Map<String, dynamic> _factories1 = {}; | |
final Map<String, InstanceBuilderCallback> _singletonFactories = {}; | |
final Map<String, dynamic> _singletons = {}; | |
Logger logger = Logger('DI'); | |
/// Shared container where all instances are registered to. A new container | |
/// may be assigned to the `sharedContainer` for testing purposes. | |
static ServiceLocator sharedContainer = ServiceLocator.newContainer(); | |
factory ServiceLocator() => sharedContainer; | |
ServiceLocator.newContainer(); | |
/// Registers a factory closure. A new instance will be created every time it | |
/// is resolved. | |
void registerFactory<T>(InstanceBuilderCallback<T> function) { | |
logger.info('Registered $T.toString() as a factory'); | |
_factories[T.toString()] = function; | |
} | |
/// Registers a factory closure with one argument. A new instance will be | |
/// created every time it is resolved. | |
void registerFactory1<T, A>(InstanceBuilderCallback1<T, A> function) { | |
logger.info( | |
'Registered $T.toString() as a factory1 with a $A.toString() argument'); | |
_factories1[T.toString()] = function; | |
} | |
/// Registers a singleton factory closure. The instance will only be created | |
/// once resolved. | |
/// Arguments are not supported for singleton registration. | |
void registerSingleton<T>(InstanceBuilderCallback<T> function) { | |
logger.info('Registered $T.toString() as a Singleton'); | |
_singletonFactories[T.toString()] = function; | |
} | |
/// Calls `resolve` under the hood. This method does not support arguments. | |
T call<T>() => resolve(); | |
/// For singleton registration, it returns a previously created singleton | |
/// instance if already created, otherwise creates a new instance and caches | |
/// it. For regular factory, it returns a new instance value every time. | |
T resolve<T>() { | |
final name = T.toString(); | |
if (_singletons.containsKey(name)) { | |
logger.info('Resolved $name as a Singleton'); | |
return _singletons[name] as T; | |
} | |
if (_singletonFactories.containsKey(name)) { | |
logger.info('Created and Resolved $name as a Singleton'); | |
final instance = _singletonFactories[name]!(ServiceLocator()) as T; | |
_singletons[name] = instance; | |
_singletonFactories.remove(name); | |
return instance; | |
} | |
if (_factories[name] != null) { | |
logger.info('Resolved $name as a factory'); | |
return _factories[name]!(ServiceLocator()) as T; | |
} | |
throw Exception("Instance hasn't been registered!"); | |
} | |
/// Returns a newly created instance injecting one argument. | |
T resolve1<T, A>(A arg1) { | |
final name = T.toString(); | |
if (_factories1[name] != null) { | |
logger.info('Resolved $name as a factory1'); | |
return _factories1[name](ServiceLocator(), arg1) as T; | |
} | |
throw Exception("Instance hasn't been registered with an argument!"); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math'; | |
import 'package:test/test.dart'; | |
import 'package:common/di/service_locator.dart'; | |
class Dummy { | |
final int id; | |
final String name; | |
Dummy(this.id, this.name); | |
factory Dummy.named(String name) { | |
return Dummy(Random().nextInt(1000000), name); | |
} | |
} | |
void main() { | |
ServiceLocator sl = ServiceLocator.newContainer(); | |
setUp(() { | |
sl = ServiceLocator.newContainer(); | |
}); | |
group('ServiceLocator', () { | |
test('registerFactory creates a new instance on every resolve', () { | |
sl.registerFactory<Dummy>((_) => Dummy.named('John')); | |
final firstResolve = sl.resolve<Dummy>(); | |
final secondResolve = sl.resolve<Dummy>(); | |
expect(firstResolve, isNot(equals(secondResolve))); | |
}); | |
test('registerFactory1 creates a new instance on every resolve1', () { | |
sl.registerFactory1<Dummy, String>((_, name) => Dummy.named(name)); | |
// same names but different random ids on resolve | |
final firstResolve = sl.resolve1<Dummy, String>('john'); | |
final secondResolve = sl.resolve1<Dummy, String>('john'); | |
expect(firstResolve, isNot(equals(secondResolve))); | |
}); | |
test('registerSingleton returns the same instance on every resolve', () { | |
sl.registerSingleton<Dummy>((_) => Dummy.named('John')); | |
final firstResolve = sl.resolve<Dummy>(); | |
final secondResolve = sl.resolve<Dummy>(); | |
expect(firstResolve, secondResolve); | |
}); | |
test('sharedContainer can be replaced', () { | |
final singleton = Dummy.named('Mr Singleton'); | |
// ServiceLocator() accesses ServiceLocator.sharedContainer | |
ServiceLocator().registerSingleton<Dummy>((_) => singleton); | |
expect(ServiceLocator().resolve<Dummy>(), singleton); | |
// Replace current shared container with a new one | |
ServiceLocator.sharedContainer = ServiceLocator.newContainer(); | |
// Register new instance with the same key | |
final newOne = Dummy(1, 'New One'); | |
ServiceLocator().registerSingleton<Dummy>((_) => newOne); | |
expect(ServiceLocator().resolve<Dummy>(), newOne); | |
}); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage, imagine there's a
Logging
interface and aLoggingAdaptor
class that implements it. We register the interface and return its implementation.That allows us to control the dependency flow.