Flutter dependency injection, a true love story
Working with dependency injection in flutter!
Hi, how is it going? I hope everything is good.
Today I’m going to describe some experiences that I had working with DI (dependency injection in flutter), and I hope that this article help you to use and abuse of DI with flutter.
In specially I’m going to write about injectable and get_it, because for me these two work pretty well for all types of projects, and they are easy to learn. But First, let’s understand what is Dependency Injection.
Concept
Dependency injection is the act that provides something that someone needs, example: You have the class Vehicle and this class need one Engine to work properly so, Vehicle depends on Engine. We can provide the Engine with different ways, from constructor of the class, by field injection or by method injection, later we’re going to see some examples. There are several libs and ways to handle dependency injection, but today we’re going to see how get_it does his job!
Working with dependency injection give us some benefits like have more decoupled code, so you can have different implementations for some class, like, if you have the interface Engine, you can have the implementations for electric engine and combustion engine, and provides the implementation that you want for who requires.
And another but also important helps us to have classes that are easier to test, when you receive your dependencies through the constructor for example you can easily mock the behaviour of these dependencies and write the tests that you need.
Want to read this story later? Save it in Journal.
Get It
This library use the technique of service locator (think of a class that holds all information, and when you need something you call her and get what you need) to provides the dependencies that you need, this is a more simple way to handle but works just fine for your needs.
Before we get started, add in your pubspec.yaml the following dependency. Go to the package page on pub.dev and get the latest version!
get_it: x.x.x
After you run pub get, we can start the work. To access the singleton of GetIt you can use GetIt.instance. You can have multiple GetIt instances, but I do not recommend, and I don’t know why you need more than one instance but wherever. Before we can inject your instances on our classes, we need to register each of them.
Important: GetIt works through the order that you declare your dependencies so, if Vehicle depends on Engine, you shall declare engine before vehicle, otherwise you’re going to receive an error in your face!
To declare a dependency, GetIt provides some methods, and each of them has his purpose. I’m going to explain each of them and when do I think that is a good idea to use them.
Critical: You need to declare all of yours dependencies as soon as the app starts, so you can access them all at any time. Normally you do that before the runApp function.
Singleton
A singleton is a class that have only one instance through your application, it is like the instance of GetIt, so if you request the instance of class A, in class B and C, both going to have the same instance, you have two ways to declare a singleton with GetIt
Critical: Be aware of using singletons, each singleton that you use consumes memory, so you should use them only when necessary, like, you have one repository that holds some cache, or you’re going to provide the instance of shared_preferences. Use them wisely to have more memory for the things that you need most.
In the following example, we’re going to use the classes above!
First way:
<Engine> Here you pass the abstract class if you have
EletricEngine() And here the implementation of the class
In this approach, you’re going to have only one instance of EletricEngine for your application.
Second way:
In this way you also have only one instance, but the difference is that GetIt will only create the instance of EletricEngine when someone request it, that’s why we pass a function () →, doing that we can save some memory. For example, you have the screens A, B and C, A opens B and B opens C, and you only use the ElectricEngine on the screen C, so you don’t need to create it on A or B screen, but at the moment that you create the instance it will be there until you close the application.
Use a lazy Singleton is great, but you don’t need to put lazy for everything because the most of the dependencies that you use are used right away, like some network class that you do a request right when the apps open.
When using singletons you do not need to have an abstract class you can also do like this. Having the abstract helps you to have different implementation and following the S.O.L.I.D principles you should aways depend on interfaces or abstract classes and not concrete classes. You can read more here.
Factory
Factory it is the most used method because when you declare something with factory, each time that you request the instance of some class you’re going to have a new instance of it, so you do not have something holding the instance like a singleton, that’s why we declare using a function () → like lazy Singleton.
Using
Like I said before, we have some different ways to provides the instance of some class, I’m going to show two of them.
Constructor
We’re gonna provides the instance of engine through constructor method for vehicle class.
We just need to register to GetIt both classes and all is set
Doing this, when you use the class vehicle, she will have the eletricEngine inside that was provided thought constructor parameter by getIt.
Field injection
We’re gonna provides the instance of the eletricEngine by field and not by constructor. The getIt variable is a global singleton class, so you can access it from any place. You do not need to specify the interface, like getIt<Engine>()
if you have already declared the type of the variable. In dart, it is a good particle to declare the type variables that are global in the class, you can omit in local variables, like the ones inside some method.
Doing this, you have the same results of injection through constructor, but the difference is getting it inside the class. Using this approach, it is more difficult to test your classes because, when you create your unit test, you’re gonna need to mock all the declarations inside getIt before. If you pass them through constructor, you don’t need to worry about getIt.
Fields injection are great to inject something inside your widgets, but the rest I recommend you to use constructor injection!
You can read more about GetIt in here.
Getting seriously
Now that we see how to use GetIt we’re going to use the injectable package to do the job for us. As your project grows, it becomes hard to manage and control all those declarations, so that’s where injectable shine. This package works alongside of GetIt and generate all these declarations using some annotations and build_runner.
First we need to add the dependency on pubspec.yaml, get the latest versions of injectable here and injectable_generator here.
dependencies:
injectable: x.x.xdev_dependencies:
injectable_generator: x.x.x
build_runner: x.x.x
We declare injectable_generator as dev_dependency because we do not need this on production code because the function of this package is to read the annotations that we add in our code, and generate all the code that we need, so no need to increase the size of our app right?
After that, we need to declare our entry point of the application.
What’s happening here? First we are getting the instance of the GetIt, and passing it to the $initGetIt method, this is a method that will be generated inside the class di.iconfig.dart. But for this code generation to happen, we need the @injectableInit to inform that this is the start point, so it gets the GetIt instance to use inside the generated class. Use the following command to generate the code.
flutter packages pub run build_runner build
After that we just need to call this function before we start the app, so when we request the dependencies they will be registered.
Important: You just need the runZonedGuarded if you have a dependency that needs to be awaited, like a sharedPreferences instance, so you pass the ensureInitialized to tell flutter to wait until everything is ok to start the app, if this is not your case you can use:
And of course your configureInjection will return void:
Right now, we are ready to declare our dependencies!
Declaring
Important: The code generation works with constructor and field injection types, but if you are using the field type, you still need to use getIt<T>()
to recover the instance.
Factory
To declare a factory you just need to use the @injectable
annotation and when the code generation finish we're going to have the same code that we right on the examples above.
You only need to pass the as: if you are injecting something that has an abstract class, in case that you do not have use @singleton or @injectable … This is valid for all annotations!
Remember, all generated code will appear on di.iconfig.dart.
Singleton
LazySingleton
This follow the example that I said before, if you need something that is not used right away, you can declare using lazy.
Module
Injectable offers one more handful annotation @module when you declare a module, you need to create an abstract class like this:
Modules are helpful when you need to provide a third part package like SharedPreferences or Dio. So you can customize how the instance will be provided. You can also use the @preResolve annotation when you have an asynchronous instance like SharedPreferences so GetIt will await for the resolution of this instance to continue, and it is in cases like this you need the runZoned function like I explained before.
Named
Using the example that I said before of Vehicle and Engine that have EletricEngine and CombustionEngine we can declare names for different instances of the same interface. Here we have A remoteService interface and two implementations, the default one that should consume some API, and the mock one that access some local JSON. Using the @Named you can declare different instances.
So when we inject them we just need to pass the name of the instance that we want and injectable will do all the work for us.
At the end when he generates the code he creates some sort of map that hold the instance or the function that create the instance of the mock or the default one. So when you pass the name, he looks for it and return the right instance.
Notes
In flutter, we have several ways and packages to handle dependency injection, I decided to do this post about getIt and injectable, because for me, they are great packages, you can learn easy how to use them, and your code gets easy to understand too, of course if you try to read the generated file, it is a problem but 99% of the time you don’t even know that he exists.
Saying that. You should look at your needs and your projects and choose the best approach to handle dependency injection, because maybe you have a simple project that doesn’t need all these, so take some time to think about it. But my advice is even if you have a simple project remember, everything changes at some point so, just think in the future and architecture your project thinking on possible changes.
Tips
This post is a compiled text of all the things that I learn using these packages, but you can still access their pages and read the documentation, like injectable you can use environment like dev, prod and provides different instances or, if you have a concise name convention you can use configuration files to generate injection for them without the need to add annotations. I am strong to recommend the reading of the documentation if you want more details.
Use the flag, to remove the files that are already generated, in some cases build_runner doesn’t generate all the files because he gets confused with some conflicts.
— delete-conflicting-outputs
And use the command, to keep watching all the changes that you make to the files, so you don’t need to restart the build every time.
watch
Full command:
flutter packages pub run build_runner watch — delete-conflicting-outputs
Important: When you run build_runner, he generates code for all packages in your project, so if you also use for example json_serializable it will also generate files for them. There is no problem to generate a file again, this is just essential information.
Keep your packages always updated, they are constant evolving and releasing some cool features.
You can get the example project here, this repository is also an example of using clean architecture on flutter, that I wrote in here.
#flutter, #di, #getit, #injectable