Compile time Dependency Injection in Flutter

Compile time Dependency Injection in Flutter

·

0 min read

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

  1. What is Dependency Injection?
  2. Adding inject.dart package as a submodule to the project
  3. Understanding the annotations(@provide, @module etc)
  4. Writing modules and Injector class
  5. Resolving dependencies at compile time 😍
  6. 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 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:

  1. A circuit board containing the brains of the phone.
  2. An antenna.
  3. A liquid crystal display (LCD)
  4. A microphone.
  5. A speaker.
  6. 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(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 inject.dart 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 pubspec.yaml:

  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 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.


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 dependent modules as an argument because that is how the Injector 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 both MoviesBloc 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 for my work by liking this article. Feel free to connect with me at Twitter or LinkedIn for any help or query.