What is Dependency Injection? Why do we need it?

What is Dependency Injection? Why do we need it?

Get to know about one of the best practices in Android Development

When learning to develop Android apps, we come across a concept called Dependency Injection ๐Ÿ’‰๏ธ. We also hear about lots of Libraries used to inject dependencies into the application.

But if you are overwhelmed by the term Dependency Injection, Hilt, Dagger, etc., then you are at the right place.

In this series of articles, I'm going to simplify what is dependency injection, why do we need it, and how to implement them.

In this series, I'll use Kotlin which is my preferred language to develop Android Apps but this concept holds good for other Object-Oriented Languages too.

Dependency

To understand the concept of Dependency Injection, we need to know what a dependency is.

In object-oriented programming, a dependency is some object which our class is dependent on to function properly and is an instance of another external class.

Sounds complex right... I'll make things simpler with an example.

Suppose we are building a Gaming PC ๐Ÿ˜๏ธ.

Gaming PC

Translating the scenario in Kotlin, we would have something like this...

class Computer {

    fun onComputerPowerOn() {
        // Something which we would like to do when we turn the computer on
    }

    fun onComputerShutDown() {
        // Something which we would like to do when we shut the computer down
    }
}

Now, to build a useful Gaming PC, we need the following things

  • Processor
  • RAM
  • Graphic Card
  • Motherboard
  • Hard Drive

just to name a few...

Now, if we implement this in Kotlin, the code would look something like this

class Computer {

    var processor: Processor =  Processor("Intel i5")
    var gpu: GraphicsCard = GraphicsCard("NVIDIA GeForce MX250")
    var RAM: RandomAccessMemory = RandomAccessMemory(8)
    var hdd: HardDrive = HardDrive(512)

    fun onComputerPowerOn() {
        // Something which we would like to do when we turn the computer on
    }

    fun onComputerShutDown() {
        // Something which we would like to do when we shut the computer down
    }
}

NOTE: This is done with an assumption that we have appropriate classes for Processor, GraphicsCard, RandomAccessMemory, and HardDrive

If you notice, we have an instance of all the required components inside our Computer class. Now, the computer depends upon all these instances and this is what we call a dependency.

And creating a Computer would be as simple as instantiating Computer class inside our main function

fun main() {
    var myGamingPC: Computer = Computer()
}

To conclude, A dependency is the objects on which the class depends upon.

Dependency Injection ๐Ÿ’‰๏ธ

Now that we know what dependency is, it's time for us to look into Dependency Injection ๐Ÿ’‰๏ธ.

Dependency Injection is something that we all are familiar with at a basic level.

When a class has some dependencies, we must somehow provide the class all those dependencies to make it work.

Now, if you observe in the above example, we have provided the dependency inside the class by instantiating the objects inside the class itself.

Now, this is BAD as it increases coupling.

Constructor Injection

One way to provide the dependencies is by using constructors.

Now, if we use constructor to inject dependencies in our above example, the class would look something like this

class Computer(
    val processor: Processor,
    val gpu: GraphicsCard,
    val RAM: RandomAccessMemory,
    val hdd: HardDrive
) {

    fun onComputerPowerOn() {
        // Something which we would like to do when we turn the computer on
    }

    fun onComputerShutDown() {
        // Something which we would like to do when we shut the computer down
    }
}

And we must pass all the required dependencies while creating the Computer object.

fun main() {
    var processor: Processor = Processor("Intel i5")
    var gpu: GraphicsCard = GraphicsCard("NVIDIA GeForce MX250")
    var RAM: RandomAccessMemory = RandomAccessMemory(8)
    var hdd: HardDrive = HardDrive(512)

    var myGamingPC: Computer = Computer(processor, gpu, RAM, hdd)
}

By doing so, we have injected the dependency using constructor and this process is called Constructor Injection.

Well if you were into Object-Oriented Programming for quite some time.. you might be thinking, "I have been doing this regularly, and what's new in this". That's why I said we are familiar with Dependency Injection at a basic level.

There's not much new in this and it's just a way to inject dependencies into our objects.

We can follow several other approaches to inject the dependencies and Constructor Injection is just one of them.

Libraries like Hilt and Koin help us to inject dependencies and also by providing a bunch of functionalities like deciding the scope of our dependencies [Bear with me for some moment if you don't know what scope is].

Now, we come to the question Why should we follow Dependency Injection?

The need for Dependency Injection ๐Ÿ’‰๏ธ

Dependency Injection helps us in optimizing how we use our dependencies.

We will continue with our previous example of Computers.

Suppose we add another dependency say a webcam, then we don't need the webcam to be active every time we make use of the computer.

We need it only when we are recording a video or attending a video conference.

Similar cases arise while developing an app where we would need certain objects only when a particular action is carried out and not during the whole lifetime of the application.

Dagger-Hilt, which is a library used for Dependency Injection, provides this functionality with the help of Component Scopes. [Now this is what I mentioned about that libraries help in deciding the scope of our dependencies]

We would see more on Dependency Injection with Dagger-Hilt in a separate article.

Dependency Injection also helps us to test our code with objects of different qualities.. for example, consider the following code snippet.

fun main() {

    var processor1: Processor = Processor("Intel i5")
    var gpu1: GraphicsCard = GraphicsCard("NVIDIA GeForce MX250")
    var RAM1: RandomAccessMemory = RandomAccessMemory(8)
    var hdd1: HardDrive = HardDrive(512)

    var processor2: Processor = Processor("Intel i7")
    var gpu2: GraphicsCard = GraphicsCard("AMD Radeon RX 6000")
    var RAM2: RandomAccessMemory = RandomAccessMemory(16)
    var hdd2: HardDrive = HardDrive(256)

    var myGamingPC: Computer = Computer(processor1, gpu1, RAM1, hdd1)

    var alsoMyGamingPC: Computer = Computer(processor2, gpu2, RAM2, hdd2)
}

In this, we've created two different computers with different specifications and we can use both of them to test different use cases.

Similarly, in an Android app that follows MVVM Architecture, we can inject different implementations of repositories and test our code with all those versions.

There are lots of interesting things we can uncover with the help of Dependency Injection but for now, I would like to wrap it up with this.

I hope you would have got a pretty good understanding of Dependency Injection and we would look at implementing it in upcoming Articles.

Have Fun Coding! ๐Ÿ‘‹

ย