In this article first we will understand what exactly is dependency injection in android and after that we will learn applying the concept of dependency injection in our Android projects. A dependency injection is one of the most important Design patterns that you must understand As a Software Engineer. Dependency injection is a technique that is widely used in software engineering to eliminate coupling of code. That is why the concept is also used while building Android apps.
Ways to do dependency injection in Android:
- Constructor Injection. By using constructor injection you can pass the dependencies of a class to its constructor.
- Field Injection (or Setter Injection). Some Android classes such as activities and fragments are instantiated by the system, so you can not do constructor injection. In such case you can use field injection after that dependencies are instantiated after the class is created.
- Method Injection:-To use method injection you can use @Inject annotation with method.
Let’s understand about dependencies and dependency injection with this very simple Kotlin code example.
Class Car{
private val engineInstance:Engine
Init
{
engineInstance=Engine()
startCar()
}
private fun startCar()
engineInstance.startEngine()
}
Here, there is a class called car, It has an instance variable called engine Instance of type Engine In the init block , the car constructs a newengine object and assigns that to Engine instance variable and later it calls to the startCar function, We can see Car object uses that instance variable to call the start function of newly created Engine object
So in this example you can see the Car class depends on the Engine class .Car class cannot work without the support of Engine class. Wherever Cars are Engines needs to be there. We cannot reuse Cars without reusing Engines. So we can consider Engine class as a dependency for the Car class. Car class can also be called as the dependent of the Engine class. In this case Cars depend on engines. But Engines may not depend on Cars.
Now let’s have a look at the Engine class.
Class Engine{
private val pistonInstance:Piston
Init
{
pistonInstance=Piston()
startCar()
}
private fun startEngine()
//code to start engine
}
Looks like this Engine class have a dependency called Piston. Engine object cannot survive without the support of the Piston object for this case Engine class becomes the dependent and the Piston class become the dependency. Now, what can you say about Car object? looks like we have just found an indirect dependency called Piston for the Car object.
I just showed you a very simple Kotlin example about dependencies , In a real world large project you will find many dependencies, We may create some of them, and we may also use third party dependencies. If you are using Retrofit to communicate with a REST API, retrofit instance becomes a dependency. If you are going to use Room, room data base instance becomes a dependency. if dependencies are tightly coupled , It’s very hard to do testing, bug fixing and expanding of the code .We should embrace loosely coupled and highly cohesive dependencies. That’s why we are going to do dependency injection. In industrial level large projects we must pay attention to follow dependency injection architectural pattern in order to avoid unnecessary problems that can arise during the later stages of the project.
how can we write codes to achieve this. If we try to do it manually we will have to deal with complex code logics. If you are a really good , well experienced developer you will be able to somehow do it, but it will take a lot of time and effort. That’s where Dependency Injection Frameworks comes to help us.
Among those dependency injection frameworks dagger 2 is the most successful and most popular framework. Dagger makes our life easier by generating all the codes for us. So let’s understand what are the problems that we have with this approach.
The first problem is Car and Engine are Tightly Coupled as Car is constructing its own engine we cannot or we cannot easily use different implementation of Engine for Car.
For Example, we have to create two cars one is for gas engine and another one is for electric engine then reusing the same Car here is not easy in the given code because Car is constructing its own Engine passing a Test Double for Car to test different use-cases is also not possible. For testing many times we mock objects that are called Test Double. So I hope you understood what are the issues with this approach.
Now let’s move to a very simple solution instead of Car to construct its own Engine this time we are passing the Engine as a constructor parameter.
Class Car(private val engine:Engine)
{
fun start()
{
engine.start()
}
}
Now, this approach will solve all the problems that we discussed. Now Car and Engine are not Tightly Coupled as we are getting the engine from outside the Car.
We can easily pass different Engine implementations to Car while constructing a Car. Passing Test Double is also easy now for testing. A simple change solved almost all the issues that we had and this approach is called Dependency Injection. The Engine is the dependency to the Car and now we are injecting dependency via a constructor. So it is also called Constructor Injection.
There are two main types of Dependency Injection
first one that we have already seen is called Constructor Injection another solution is called Field Injection as you can see here in both approaches Car is not constructing its own Engine but the Engine is being injected into the Car. In both examples, we are constructing the Engine manually and hence it is called Manual Dependency Injection.
It is an ok solution but it also has some issues. Right now we have just seen a simple example but think about bigger projects. For example, Engine may also require a lot of other Dependencies. So handling all dependencies manually may end up to a lot of boilerplate code that will be hard to maintain and if you are creating dependencies manually then you also need to manage the life cycle of dependencies in memory that can be a tedious task. To tackle these problems we have automated dependency injection solutions that can be categorized two types the first one is Reflection based solutions that generate the dependency and runtime and static solution that generates the code to provide dependency and compile time. In this course we will learn about Dagger and Hilt both are the static solution of Dependency Injection as both libraries generate Dependency Injection code at compile time. In fact, Hilt is the recent library that is built over the top of Dagger and now is the recommended solution by the Android jetpack, and Dagger2 is also maintained by Google and it is also the recommended DI for Android projects. So dependency Injection enhances code reusability and decouples dependencies. After Dependency Injection the Dependencies are not hidden as implementation details. So it is easy to refactor the dependencies now and after using Dependency Injection testing also becomes easier. So these are the some reason to use Dependency Injection in your project.