kfx_dependency_injection 1.5.0 kfx_dependency_injection: ^1.5.0 copied to clipboard
Flutter Dependency Injection library based on dotnet ServiceProvider
kfx_dependency_injection #
A simple dependency injection and service locator inspired by .net service provider.
In software engineering, dependency injection is a design pattern in which an object or function receives other objects or functions that it depends on, so you can pass some abstract classes (the closest thing as an interface you can get in Dart) and let this package figures out what your class will get when it is instantiated.
It also works as a service locator (when you have an abstract/interface definition and want to choose the concrete implementation on the registration).
Features #
- Allows registration of transient and singleton dependencies
- Covers misconfigured lint options (such as using generic methods without providing generic types)
- The injector has a
PlatformInfo
available, so you can decide what to inject based on media (web, desktop or mobile) and host (Windows, Linux, MacOS, Android or iOS) - Flutter web safe, including
PlatformInfo
- No external dependencies
- Services can say when they must be transient or singleton by implementing
IMustBeTransient
orIMustBeSingleton
Usage #
For example, you can define an authentication system as a set of empty common methods (in an abstract class, which is the closest you can get right now to a true interface in Dart). Then, you implement the authentication itself on another class, let's say, using Firebase authentication. Wanna use AWS Incognito? Just reimplement that abstract class using Incognito e change the registration to point to it. Done, without breaking changes.
Log is important and you can also do the same for logging. Perhaps using dart:developer
to make it so.
When you register the authentication service, you can inject the log service on it (at that point, it is an abstract class that doesn't care how it will be implemented). Change the log registration to some other type (perhaps some remote logging?) and everything keeps working perfectly, and you don't even need to touch the authentication service (since all is based on interfaces/abstract classes, which are only contracts to concrete implementation)
So the registration on main
is something like this:
ServiceProvider.registerSingleton<IAuthenticationService>(
(optional, required, platform) => FirebaseAuthenticationService(
logService: required<ILogService>()
)
);
ServiceProvider.registerSingleton<ILogService>(
(optional, required, platform) => DartDeveloperLogService(isWeb: platform.platformMedia == PlatformMedia.web)
);
You can call optional<TService>()
to get an optional service (which will return null
if not registered) or required<TService>()
to get a required
concrete implementation of the desired TService
.
Notice that the order of registration doesn't matter, as long as you register all dependencies before using them (a good place is the main
method, before your app runs).
The platformInfo
argument is an instance of the PlatformInfo
, so you can instantly know what kind of media you are using (Flutter Web, Flutter Desktop or Flutter Mobile)
and the host you are running (Android, iOS, Windows, MacOS or Linux). That info is separated between media and host, so you can know when you are running Flutter Web on an
Android device, for instance (perhaps to choose the appropriate design system (i.e. Material, Apple or Fluent)). In the example above, I could tell my concrete log implementation if we are running Flutter Web or native.
Now, to get your authentication service, with the injected log stuff defined in the registration, you just need to:
final authenticationService = ServiceProvider.required<IAuthenticationService>();
And that's it. You don't need to know the concrete implementation nor know what kind of log is being used (or if it even exists).
Singleton vs Transient #
The difference between the registrations is: whenever you call optional
, required
or something is injected in another constructor, singletons always
returns the same instance of a class, while transient registrations always return a new instance of that class (in most cases, you want a singleton).
optional vs required #
The difference between those methods is that optional
can return null
if the specified service was not registered, while required
will throw a
ServiceNotRegisteredException
if the service was not registered. You should use required
to ensure everything was correctly registered during app's initialization.
Mocking #
After registering a type, you can override it using ServiceProvider.override<TService>((optional, required, platform) => MockClass(), key: "some key")
.
This is useful when you use some remote API service registered on your app, but want to mock that service in unit tests. In this case, the app remains as it is (no change is required). In your unit test, you override the registration of your API calls to some mock class and you're done.
That override can take place before or after the normal registration (i.e.: you can override before instantiating your app and registering your types or after that, it doesn't matter)
Additional information #
There are some methods to check if a type is registered (isRegistered<T>()
) and methods to allow unregistration (this is useful to release singleton instances or
in unit tests to clean up the ServiceProvider
manager).
Since Dart is incapable of returning unique type names, all methods in ServiceProvider
accepts a key, which will be used to differentiate types.
For instance: the firebase_authentication
package contains an User
class. It's probable that you also have a User
class in your code, which has nothing to do
with that Firebase implementation. The problem is that Dart will return User
in both cases when we ask for the name of the type. That's the same reason you have to use
the as
, hide
and show
keywords during import
to avoid class and function names conflicts.
So, if you have two classes with the same name and want to register them (since it's not possible to register a type more than once), you can differentiate them using
the key
argument:
import 'some_class.dart';
import 'package:some_package:some_class.dart' as SomePackage;
ServiceProvider.registerSingleton<SomeClass>(
(optional, required, platform) => SomeClass()
);
ServiceProvider.registerSingleton<SomePackage.SomeClass>(
(optional, required, platform) => SomePackage.SomeClass(),
key: "SomePackage"
);
The second registration must use the key
argument because the SomeClass
exists in multiple locations and was already registered.
To retrieve each version of the registration, use the same key:
import 'some_class.dart';
final someClass = ServiceProvider.required<SomeClass>();
This will return the first registration (because key
is null
in both cases).
import 'package:some_package:some_class.dart';
final someClass = ServiceProvider.required<SomeClass>(key: "SomePackage");
Notice that the above code doesn't have an alias anymore, but it still returns the correct SomeClass
version because the same key
argument was used.
Exceptions #
There are three available exceptions thrown by this package:
ServiceAlreadyRegisteredException
#
This exception is thrown when you try to register a type with the same key
(or lack of it).
ServiceNotRegisteredException
#
This exception is thrown by required
when it requires a service that was not registered and also by unregister
method if throwsExceptionIfNotRegistered
argument is true
(which is false
by default).
ServiceInvalidInferenceException
#
Dart by default will not warn you when you don't provide a generic argument and one is expected, so ServiceProvider.optional()
is valid code,
but it is impossible to know what service we should return (since TService
is dynamic
in this case). The correct usage would be
ServiceProvider.optional<SomeTypeHere>()
.
The same occurs on registration of a null type: ServiceProvider.registerSingleton((sp) => null)
which is valid code, but the type would be Null
.
To be warned about those cases, you should turn on the strict-inference
language analyzer in your analysis_options.yaml
:
analyzer:
language:
strict-inference: true
That setting will give you a warning like this one:
ServiceProvider.optional();
The type argument(s) of the function 'optional' can't be inferred.
Use explicit type argument(s) for 'optional'.
InvalidRegistrationModalForTypeException
#
This abstract exception is either RegistryMustBeTransientException
or RegistryMustBeSingletonException
and those are thrown when a service that implements
IMustBeTransient
is registered using ServiceProvider.registerSingleton
or vice-versa.