Flutter Dependency Injection in a nutshell
Explaining the concept
To begin with, let’s take a deeper look at those two blocks of code:
At first glance, both snippets look alike. That is because both of them illustrate that Product Service uses method get from DioService — which means it depends on it. The key difference is that the first example doesn’t make use of the Dependency Injection technique which makes it a lot worse than the second one. But why exactly is that so?
Why do we need Dependency Injection?
When developing an application we have to always remember that classes should be independent of their dependencies. Also as our application grows, at some point we will eventually need to separate the business logic from the widgets responsible for viewing the UI. That is why we should implement Dependency Injection pattern in a project. Not only does it help to separate different parts of your application in a more maintainable way but also allows us to have different implementations for some classes and makes them much easier to test.
On pub.dev we can find many promising packages which were created to integrate Dependency Injection into flutter projects. The most popular are:
Besides, Flutter offers itself a built-in solution — InheritedWidget. In short, it is a base class widget that propagates information down the widget tree. Though I wouldn’t recommend using it in medium and large projects since it is quite limited in comparison to the libraries listed above.
In this article, we are going to focus on get_it alongside the injectable package.
Dependency Injection vs Service Locator
Before doing the actual setup we should clarify one thing. We should keep in mind that get_it is a service locator, not a dependency injection package. Service locator is a pattern that allows us to separate the interface from a concrete implementation and to access the concrete implementation from everywhere in our application. Both service locator and dependency injection are implementation techniques of Inversion of Control principle. The difference is that the service locator technique hides dependencies whereas in the case of dependency injection they are explicit.
Setting it up
Initialization of dependency injection is very straightforward. First of all, we have to add required packages to pubspec.yaml (manually or via corresponding terminal commands). We will need injectable + get_it and build_runner + injectable_generator packages as for the dev dependencies.
Now we define a global variable for our GetIt instance and create a setup function — configureDependencies. It should be annotated with @InjectableInit and call generated function $initGetIt.
In the next step, we run the following build_runner command which will then trigger code generation. After that we can finally add configureDependencies to the main function of our application.
Done! Although we’ve made it through the setup, our app doesn’t do anything useful since we haven’t registered any classes yet.
Factories occur most often in almost every project. To register a class as a factory we simply put @injectable annotation on top of a class declaration.
Now we let build_runner do the work by running the code generation command mentioned above. After successful execution, we may take a look at generated code.
Here, the factory is a conditional wrapper method for the GetItHelper class. Before a particular service is registered it is checked whether environment requirements are met. More information about environments can be found in the last section.
The singleton pattern is used to ensure that a class has only one instance and also provides a global point of access to it. We can register singleton classes by annotating them with @singleton or @lazySingleton depending on a use case.
Binding abstract classes to implementations
To tell injectable_generator what implementation should be assigned to what interface we just have to pass the desired abstract class as a parameter.
As a result, the following line will be added to our auto-generated config file.
Injectable package also offers an easy way to register third-party dependencies. All we have to do is to wrap a class with @module annotation. Additionally, we may add @preResolve annotation if we want to pre-await the future and register its resolved value.
After the auto-generation process we can see that besides registering a new module, the $initGetIt function has become a Future.
Registering under different environments
Injectable package supports the feature of assigning different dependencies for different environments. We can obtain it by using @Environment(“envName”) annotation. In the below example ServiceA is now only registered if we pass the environment name to $initGetIt(environment: ‘dev’).
We may also create our own environment annotations by assigning the const constructor Environment(“envName”) to a global variable.
Also, there is a possibility to assign multiple environment names to the same class. In the following example, ServiceA will be registered for both dev and test environments while ServiceB only for test.