Skip to content

Instantly share code, notes, and snippets.

@CassiusPacheco
Last active March 18, 2021 04:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CassiusPacheco/f0eea34e4136083860e8621aad217501 to your computer and use it in GitHub Desktop.
Save CassiusPacheco/f0eea34e4136083860e8621aad217501 to your computer and use it in GitHub Desktop.
A dependency resolver container for Dart/Flutter
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!");
}
}
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);
});
});
}
@CassiusPacheco
Copy link
Author

CassiusPacheco commented Jan 22, 2021

Usage, imagine there's a Logging interface and a LoggingAdaptor class that implements it. We register the interface and return its implementation.

That allows us to control the dependency flow.

final sl = ServiceLocator(); // this is the same as ServiceLocator.sharedContainer

sl.registerFactory<Logging>(container) => LoggingAdaptor('Default'));
sl.registerFactory1<Logging, String>((container, title) => LoggingAdaptor(title));

// Examples of resolving dependencies
Logging log = sl.resolve();
log.info('test resolve');

Logging log1 = sl.resolve1('Some Logger Name');
log1.info('test resolve1');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment