Fast service locator for Dart and Flutter with support for deep injection params.
No dependencies, no code generation.
Getting started
FAQ
Q : What is the difference between using a service locator to register your objects, and turning all your objects into singletons?
A : When you turn all your objects into singletons you lose testability.
Q : What exactly should I register in the service locator?
A : In a clean architecture, only low-level classes (such as a Service or Repository which interacts with a persistence layer) should be registered. This will let you easily achieve both dependency injection into higher-level classes (such as a Controller/ViewModel) and seamless testability.
Q : Can I use this instead of some other state management solution?
A : Service locators are not state management solutions. Do not call methods on the
Registry()
object straight from your widgets. Use (constructor) dependency injection to resolve your state management Controllers/ViewModels/BLoCs etc. with the registered objects from the service locator.
Q : Why make another service locator, when there's stuff like get_it and kiwi already available out there?
A : I've had some new features in mind such as theese deep injection params, allowing one re-registration per object type and using a single method for all types of registration modes, and also to practice Dart.
Example
From registry_example.dart:
void main() {
print('1. Init service locator');
final sl = Registry()..debugLog = print;
print('2. Register object');
sl.put<IDummyClass>(
(get, params) => DummyClassImpl1(params?.byName('param') ?? 'No param'),
onDispose: (instance) => instance.dispose(),
);
final params = RegistrationParams.named({'param': 'Param123'});
print('3. Resolve object');
final object = sl.get<IDummyClass>(params: params) as DummyClassImpl1;
print('4. Check the param of the resolved object: ${object.getParam()}');
print('5. Remove object');
sl.remove<IDummyClass>();
print('6. Check if still registered');
print(sl.isRegistered<IDummyClass>());
}
Also check out test/registry_test.dart for more advanced use-cases.
Registration and resolving
The Registry
is a singleton which handles all registered objects.
Available methods:
// Register an object
Registry().put<T>(
(get, params) => YourObject(),
registrationMode: RegistrationMode.lazySingleton,
allowOneReregistration: false,
onDispose: (instance) => instance.dispose(),
);
// Get an object with optional "params"
Registry().get<T>({RegistrationParams? params});
// Check if an object is registered
Registry().isRegistered<T>();
// Refresh an existing object instance
Registry().refreshInstance<T>();
// Remove an existing object
Registry().remove<T>();
// Clear the registry, removing all objects
Registry().clear();
There are 3 available modes to register an object:
- Lazy singleton -> Single instance. It is instantiated on first
.get()
call. - Eager singleton -> Single instance. It is instantiated right when we
.put()
it. - Lazy factory -> Lazy multiple instances. We get a new instance on every
.get()
call.
When you put()
objects, you can also make sure their dependencies are automatically resolved multiple layers down:
final sl = Registry()
..put<ThirdObject>((get, params) => ThirdObject());
..put<SecondObject>((get, params) => SecondObject(get()));
..put<FirstObject>((get, params) => FirstObject(get()));
void main() {
// Automatically resolves SecondObject and ThirdObject
final firstObject = sl.get<FirstObject>();
}
class FirstObject {
final SecondObject secondObject;
FirstObject(this.secondObject);
}
class SecondObject {
final ThirdObject thirdObject;
SecondObject(this.thirdObject);
}
class ThirdObject {}
Injection params
Injection params are optional.
Params can be created in two ways:
-
By using the
.named()
constructor, in which case you can give a name to each param and access them withbyName()
:
final paramsNamed = RegistrationParams.named(
{
'first_param': 10,
'second_param': 'Test123',
},
);
final firstParam = paramsNamed.byName('first_param') as int;
-
or by using the
.list()
constructor, in which case you need to access them withbyIndex()
:
final paramsList = RegistrationParams.list(
[10, 'Test123'],
);
final firstParam = paramsNamed.byIndex(0) as int;
Now you can register an object and make use of the params field:
Registry().put<SomeObject>(
(get, params) => SomeObject(params.byName('first_param')),
);
and when you want to get that object from the Registry, add params
to `.get() and they will be passed to your object:
final object = Registry().get<SomeObject>(params: params);
Params can also be passsed from an object to another at injection time:
final sl = Registry()
// First object uses `get` to inject the second object inside itself
// and to pass the params it gets from us
..put<FirstObject>((get, params) => FirstObject(get(params: params)))
// Second object receives the params from the first object and injects it into itself
//
// We don't need to cast params here (such as 'param as int'). The type is inferred.
//
// Also, the `params` field we get in the callback is always NULLABLE.
// There's a chance we didn't get any params, that's why we use `params?.byName() ?? -1`.
//
// If you're sure you'll get some params in your callback, you can just use `params!.byName`
// without adding `?? -1.
..put<SecondObject>((get, params) => SecondObject(params?.byName('param') ?? -1));
void main() {
final params = RegistrationParams.named(
{'param': 256},
);
final firstObject = sl.get<FirstObject>(params: params);
// Now the Registry has injected the params into SecondObject, and then the SecondObject
// into FirstObject.
}
class FirstObject {
final SecondObject secondObject;
FirstObject(this.secondObject);
}
class SecondObject {
final int param;
class SecondObject(this.param);
}
Other features
-
onDispose optional callback on the
.put()
method.If non-null, onDispose will be called before the object is removed/refreshed/replaced.
We receive the current instance in the callback so we can dispose resources,
StreamSubscription
s for example.final sl = Registry() ..put<SomeObject>( (get, params) => SomeObject(), onDispose: (instance) => instance.dispose(), );
-
allowOneReregistration field on the
.put()
methodIf you try to re-register the same object TYPE twice you will get an exception.
Setting
allowOneReregistration: true
will allow you to register the same object type one more time. The new object will replace the old one entirely.This is disabled by default and in most cases it should not be needed.
NOTE: This behaviour is a one-time thing. This means that if you set this to
true
for the first registration, then you re-register the same object you must set it totrue
again if you want to re-register again (third time).// Error, allowOneReregistration is false (by default) final sl = Registry() ..put<SomeObject>( (get, params) => SomeObject(), )..put<SomeObject>( (get, params) => SomeObject(), ); // No error, allowOneReregistration is true so the second registered object has replaced the first one final sl = Registry() ..put<SomeObject>( (get, params) => SomeObject(), allowOneReregistration: true, )..put<SomeObject>( (get, params) => SomeObject(), );