Hey Folks! I am back with another brand new article on Flutter. This time I will be covering a very interesting and important design pattern in the world of software development i.e “Dependency Injection”. This will help you build classes that are loosely coupled, ultimately leading to a maintainable and testable codebase. So grab a cup of coffee, find a quiet place and learn how I use DI in a Flutter project.
- What is Dependency Injection?
- Adding inject.dart package as a submodule to the project
- Understanding the annotations(@provide, @module etc)
- Writing modules and Injector class
- Resolving dependencies at compile time 😍
- Build and run the app 🏃
Throughout this article, I will be showing you how to refactor a Flutter project which doesn’t use any DI framework. So to gain full advantage of the content, I expect that the reader has built at least 2 or 3 apps using Flutter and have some basic understanding of BLoC pattern. If you are new to BLoC pattern then these two( PART1 and PART2 ) articles are the best place to get started as I will be refactoring the same project to inject bloc and repository objects into the widget tree.
What is Dependency Injection?
Let me explain to you by taking a real-world example. Do you know how mobile phones are manufactured? Let me explain in basic terms. To build a mobile phone you basically need these components:
- A circuit board containing the brains of the phone.
- An antenna.
- A liquid crystal display (LCD)
- A microphone.
- A speaker.
- A battery.
Now all these components are not built in the same factory. These components are manufactured in different countries(assuming). Then all these individual components are imported and assembled together in one factory. This is “Dependency Injection”. The assembling factory was unaware of the build process of different components. The only job of the factory was to assemble these components to build a phone. This way the factory doesn’t need to worry about the build process of these components and can only focus on its job i.e “assemble required components to build phone”. I think this small example is enough to get started but for the curious ones who want to understand more about DI. Check this amazing article .
Let’s import MyMovies project from GitHub and run it once to check if everything is working fine.
So if you look at the project. I have the below package structure. I expect that even you have the same. Just making sure that we are on the same page:
Let me show you one example of what is the issue with the current code structure. If you open
movie_api_provider.dart file and see the second line, you will find that I am creating a new
Client() object inside the
MovieApiProvider class. But as per the explanation I gave you in the previous section about DI. This should not be the case.
MovieApiProvider should not hold the responsibility of creating the
Client object but instead, it should get the
Client object provided from someone.
This is just one example. If you check other classes like
movies_bloc.dart you will find the same case. These classes should not hold the responsibility of creating objects of the classes they depend upon. Instead, they(
movies_bloc.dart) should be provided with the required objects from some other class or factory.
Another great thing about this design pattern(DI) is, we don’t even need BlocProviders(
InheritedWidget) anymore to provide us with the bloc or any other type of object. InheritedWidget will not resolve dependencies at build time but will throw a runtime exception if failed to wire up the dependencies correctly.
So how can we remove the responsibility of creating objects from these classes? How can we create a factory which will hold the responsibility of creating and provide the dependent objects to these classes? Let’s find out. 😃
inject.dart to our rescue
We will be using inject.dart to resolve the issues I mentioned above. This is an open-sourced compile-time dependency injection package for Dart and Flutter from an internal repository inside Google. This package will help us build a factory where we can create all the dependent objects separately and inject where they are required. The best part about this package is, it will resolve all the dependencies at build time. There won’t be any runtime exceptions associated with wiring up dependencies with providers. In other words, it will make sure that all the objects are created or dependencies are resolved before you run your app. Amazing isn't it?
This framework does not “leak”, does not have side-effects, and does not require downstream dependencies to use it. This framework uses only core
dart:* libraries, so it is not platform-specific(Flutter or AngularDart).
Adding this package will be a bit different from how we add plugins to our project normally. Create a folder and name it
injection (you can name anything) inside
MyMovies the directory. Open your terminal and navigate to the folder you just created and run the below command:
git submodule add https://github.com/google/inject.dart
Now you will see the below package structure:
Ignore those errors. It won’t be a hurdle in our progress.
Add the following dependencies in your
flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 rxdart: ^0.18.0 http: ^0.12.0+1 inject: path: ./injection/inject.dart/package/inject dev_dependencies: flutter_test: sdk: flutter build_runner: ^1.0.0 inject_generator: path: ./injection/inject.dart/package/inject_generator
build_runner package provides a concrete way of generating files using Dart code. In the end, when we will be compiling the project you will the use of
Create a new package under
src and name it
di . Now add two dart files into the di package and name them as
Writing modules and Injector class
Let’s start working on the
bloc_module.dart file first. Copy and paste below code in the file:
If you have used Dagger earlier you will understand what is going on. But for others let me give you some explanation about the annotations used here. You will see some errors but don’t worry we will be fixing them soon.
Understanding the annotations
@module : This annotation will make
BlocModule class available to contribute to the dependency graph that
inject.dart will generate. It will hold all the dependencies required by other classes.
@provide : All the methods annotated with this are available for dependency injection. Checking the code above will make things clear.
@singleton : As the name suggests, it will create and provide a single instance of the object.
bloc_injector.dart and add below code in the file:
You must be seeing some errors. But don’t worry, the
bloc_injector.inject.dart file will be generated during build time and everything will get resolved. Once the file is generated then we will explore the
create() method also. One other thing to note here is the
App get app; , this is the root widget of our project and we are declaring it here so as to provide a destination where the dependencies will be injected.
@Injector : Remember we were talking about factory earlier which will assemble the dependent objects. This is what this annotation will do. It will glue everything up and make all the objects available to us for injection. This takes all the dependent modules as an argument because that is how the Injector will know what all objects need to be resolved and provided.
main.dart file and paste below code in it:
I will explain to you what this
container object is once we generate the
app.dart file and paste below code in it:
You will see some errors but we will resolve them soon. In the above code snippet, both
MoviesDetailBloc objects are provided through constructor injection and later passed on to their respective classes i.e
MovieDetail screen. No object creation here. This is freaking amazing! 😲 👏
Create a new file under
blocs package and name it as
bloc_base.dart . Paste below code in it:
movie_detail_bloc.dart file and paste below code in it:
As you can see in the above code I am not creating the
Repository object inside
MovieDetailBloc class. Instead, I have injected the object as a constructor argument. 😲 😲
movies_bloc.dart file and paste below code in it:
Same here! I am not creating the
Repository object inside the
MovieBloc class. Instead, it will be provided from somewhere outside. Wow!! 😲
movie_api_provider.dart file and paste below code:
As you can see I am not creating the
Client object inside the
@provide is used to help the DI framework to find out where all the dependencies are required and generate code accordingly.
repository.dart file and paste below code in it:
The best part is,
movieApiProvider object will be created only once because we annotated
MovieApiProvider as a
singleton while creating in the
movie_detail.dart file and paste below code:
MovieDetailBloc is injected as a constructor parameter. And there is a slight change in how the
bloc object is used in the
initState() . Feel free to explore the
init() method. I am not using the
InhertiedWidget) class anymore to get access to the
bloc object. I don’t even need a
context anymore. 😉
movie_list.dart file and paste below code in it:
Same amazing stuff here.
MoviesBloc ’s object is injected here through the constructor.
item_model.dart file and replace with this code:
I have just made a minor change. I made the Result class public so that we can pass arguments of type
Result through named routes.
We are done with the code structure change. Now we will focus on how to compile the project. This is a really important step. Create a new file under the project directory and name it as
inject_generator_build.yaml . This is where it should be placed:
Copy and paste below code in it:
We have created this file because by default all the generated files will go to cache folder and Flutter won’t be able to access the files from there(but there is work in progress to solve this). So we will change
build_to: cache to
build_to: source .
Resolving dependencies at compile time 😍
It’s time to generate some code 🔥. Open the terminal and navigate to the project folder. Run below command:
flutter packages pub run build_runner build
Build and run the app 🏃
build_runner was able to resolve all the dependencies then you will see the following output:
Congrats! If you made it till here. You have successfully generated the dependency graph. You are a genius. 👏 👏 👏 (If you run into any issues let me know your problem by putting a comment or connecting with me through LinkedIn or Twitter).
After successful code generation. You might see many generated files added to your project:
Don’t panic! This is normal. You can delete all the
*.summary files except
bloc_injector.inject.dart . You can even delete
movie_detail_bloc_provider.dart file. To make this task automated. Navigate to the lib and other sub-directories of
src and run below command(this will only delete empty files):
find . -size 0 -delete
Exploring the generated graph
WoW! This looks so beautiful ❤️. As you can see in the generated code both
MovieDetailBloc objects are passed as constructor parameters to the
App Widget. If you carefully go through this file, you will understand the purpose of
Pushing your project to git
Before you push this newly created project to your git account add the following line to your
This is a really big and powerful change we introduced to our project. Now you can easily inject any type of objects to your Widget Tree from outside. This way you will keep your codebase maintainable and testable. In my next article, I will show you how to test your code with Dependency Injection.
I have created a separate branch for this implementation. This will help you compare both the implementations(without DI and with DI) and eventually pick up whatever fits your need.
A forward-thinking Google certified android developer offering 3+ years of experience building Android applications. Currently exploring Flutter SDK for building cross-platform native mobile apps.