Dagger 2 for Android, Part I ー What is Dependency Injection?
If you're doing Android development, you might have came across the Dagger 2 library, which is used for Dependency Injection. Many Android developers find it useful, but this is a library with a steep learning curve similar to RxJava. I myself struggle to understand Dagger 2.
In my journey of studying Dagger 2, I want to write down what I've learned.
Before jumping into Dagger 2, we need to understand what is Dependency Injection. So this article will explain about this concept. 📚
Note: Examples will be given in Kotlin language with the context of Android development
This is part of my Dagger 2 blog series:
- Dagger 2 for Android, Part I ー What is Dependency Injection? (you are here)
- Dagger 2 for Android, Part II ー The Basic Usage
- Dagger 2 for Android, Part III ー The @Qualifier and @Named Annotation
- Dagger 2 for Android, Part IV ー The @Scope Annotation
- Dagger 2 for Android, Part V ー @Inject for Constructor Injection
- Dagger 2 for Android, Part VI ー @Component.Builder and @BindsInstance
Concept
There are a few types of depedency injection. In this article, we will talk about:
- Constructor Injection
- Method Injection
Each methods has it's own pros and cons. But what important is to understand what Dependency Injection itself is, and the trade-off between them. Let's take a look at the first method Constructor Injection to begin with. 💪
Constructor Injection
Say we have a Person
name John
class Person {
val name: String = "John"
}
In the caller side, we can print the name like this 👇
fun main(args: Array<String>) {
val john = Person()
println("the name is ${john.name}")
}
Now imagine that we need to have a Kelvin
, but we cannot change the name to Kelvin
. Because whenever we make an instance of Person
. The name
will automatically become John
.
To solve this, we can apply dependency injection concept.
Person
depends on name
, so we can say name
is a dependency of Person
. "Dependency Injection" simply means injecting this dependency into Person
.
This can be achieved by making name
a constructor parameter.
class Person(val name: String)
Now in main.kt
, we can simply inject the dependency, name
into it.
Here's a re-write of the code for John
:
fun main(args: Array<String>) {
val john = Person("John")
println("the name is ${john.name}")
}
If you need a Kelvin
person, sure!
Just pass the name "Kelvin"
into the constructor:
fun main(args: Array<String>) {
val kelvin = Person("Kelvin")
println("the name is ${kelvin.name}")
}
🎉 Hooray! That's dependency injection (constructor injection)! 🎉
Method Injection
Sometimes, the dependency is not ready when your object is being created. In this situation, we cannot use the Constructor Injection
, and we have to use Method Injection
. Let's consider an example.
We'll start with a Dinosaur
class this time.
This is the class for Dinosaur
. The name
is not known at first, and it will be fetched from the network, so it is marked as a nullable
type, String?
.
class Dinosaur(var name: String? = null)
In our MainActivity.kt
, we create a Dinosaur
, at this point, this dinosaur
has no name
.
class MainActivity: AppCompatActivity {
val dinosaur = Dinosaur()
fun onCreate() {
setContentView(...)
}
}
Let's pretend it looks like this:
Next, we add the code to fetch name
from network:
class MainActivity: AppCompatActivity {
val dinosaur = Dinosaur()
fun onCreate() {
setContentView(...)
val name: String = fetchNamefromNetwork() // pretend it's a network call
dinosaur.name = name // inject the name once we obtain from network call
nameTextView.text = dinosaur.name
}
fun fetchNamefromNetwork(): String {
Thread.sleep(1000) // fake delay to simulate network call
return "T-Rex"
}
}
You can see in the code above that, at the beginning, dinosaur
is instantiated with no name
. And after the name
is obtained from the network, then only we update the dinosaur.name
.
This is method injection.
Here's the T-Rex 🎉
When to use Method Injection?
If the dependency is not ready at the time we create the object. In this example, when we create Dinosaur
, name
is not ready, so it's suitable for this case.
this is a trivial example to explain the point, in practical case, we can create Dinosaur
object only after name
is obtained.
The downside of this method is that we need to make sure that the dependency is ready when we use it. In our Dinosaur
example, name
is marked as nullable
, so we will always have to check before using it.
dinosaur.name?.let { nonNullName ->
// use nonNullName
}
If it is possible, constructor injection should be used to simplify your code.
You should have enough understanding to get started with Dagger 2.
Stay tuned for the next post!
Tan Jun Rong
Clap to support the author, help others find it, and make your opinion count.