Dependency Injection in BLoC pattern
Compile time Dependency Injection in Flutter
Dependency Injection like Dagger in Flutter
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.
Content
- 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 🏃
Goal
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 1 or 2 apps using Flutter and have some basic understanding of BLoC pattern. If you are new to BLoC pattern then you can check out the below articles which are part of the series “Architect your Flutter project using BLoC pattern”. It’s the best place to get started as I will be refactoring the same project to inject bloc and repository objects into the widget tree.
Other parts of the series
Part 1
Part 2
Part 4
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.
Setup
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:
The Problem
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 repository.dart
and 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(repository.dart
or 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
(InheritedWidgets
) anymore to provide us with the bloc or any other type of object. InheritedWidgets
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 inject.dart
Adding this package will be a bit different from how we add plugins to our project normally. Create a folder and name itinjection
(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 pubspec.yaml
:
dependencies:
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
The 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 build_runner
.
Create a new package under src
and name it di
. Now add two dart files into the di
package and name them as bloc_injector.dart
and bloc_module.dart
.
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 makeBlocModule
class available to contribute to the dependency graph thatinject.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.
Now open 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 dependentmodules
as an argument because that is how theInjector
will know what all objects need to be resolved and provided.
Open main.dart
file and paste below code in it:
I will explain to you what this container
object is once we generate the bloc_injector.inject.dart
file.
Open 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 MoviesBloc
and MoviesDetailBloc
objects are provided through constructor injection and later passed on to their respective classes i.e MovieList
and 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:
Open 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. 😲 😲
Open 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!! 😲
Open movie_api_provider.dart
file and paste below code:
As you can see I am not creating the Client
object inside the MovieApiProvider
class. @provide
is used to help the DI framework to find out where all the dependencies are required and generate code accordingly.
Open 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 BlocModule
.
Open movie_detail.dart
file and paste below code:
Here 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 MovieDetailBlocProvider
(InhertiedWidget
) class anymore to get access to the bloc object. I don’t even need a context anymore. 😉
Open movie_list.dart
file and paste below code in it:
Same amazing stuff here. MoviesBloc’s
object is injected here through the constructor.
Open 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 🏃
If 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).
Cleanup
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 *.inject.dart
and *.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 bothMoviesBloc
and MovieDetailBloc
objects are passed as constructor parameters to the App
Widget. If you carefully go through this file, you will understand the purpose of BlocModule
and bloc_injector.dart
file.
Pushing your project to git
Before you push this newly created project to your git account add the following line to your .gitignore
file:
*.inject.summary
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.
I hope you liked the article. Do show your love to my work by putting a star on my Github repo. Feel free to connect with me at Twitter or LinkedIn for any help or query.
📝 Read this story later in Journal.
👩💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.