Dependency Injection with Dagger 2 in Android

Photo by Zeke See on Unsplash
Photo by Zeke See on Unsplash
Dependency Injection (DI) has been a very popular technique in recent years. In Android, Dagger 2 is the most commonly used DI framework.

Dependency Injection (DI) has been a very popular technique in recent years. As the code becomes larger, we must have a technology to help us manage the structure of the code and maintain the decoupling between modules. In AndroidDagger 2 is the most commonly used DI framework.

The complete code for this chapter can be found in .

Dependency Injection

When using dependency injection, all objects to be injected must have a corresponding Interface. As shown in the figure below, both OrderManager and OrderRepository are Interface. So we can inject the implementation at runtime that are OrderManagerImpl and OrderRepositoryImpl.

Order Diagram
Order Diagram

The following is their code.

data class Order(
    val name: String,
    val price: Double,
)
interface OrderRepository {
    fun findAll(): List<Order>
}
class OrderRepositoryImpl : OrderRepository {
    override fun findAll(): List<Order> = listOf(
        Order("CocoCola", 1.5),
        Order("Fries", 1.25),
        Order("Burger", 5.59),
    )
}
interface OrderManager {
    fun getList(): List<Order>
}
class OrderManagerImpl(private val repository: OrderRepository) : OrderManager {
    override fun getList(): List<Order> = repository.findAll()
}

We can implement dependency injection manually.

val repository = OrderRepositoryImpl()
val manager = OrderManagerImpl(repository)
val orders = manager.getList()

Dagger 2

In the previous section, we saw that dependency injection can be implemented manually. However, it will be very troublesome if you have to manually implement it every time you need to use it. Dagger 2 can help us simplify this procedure.

Before we start to use Dagger 2, we first need to add the following dependencies into the module level app/build.gradle.

plugins {
    id 'kotlin-kapt'
}

dependencies {
    implementation 'com.google.dagger:dagger:2.40.5'
    kapt 'com.google.dagger:dagger-compiler:2.40.5'
}

@Module & @Provides

Next, we need to declare a @Module class to tell Dagger which interfaces need to instantiate and how to instantiate those classes. @Module contains a bunch of @Provides methods. The return type of @Provides method is the interface to be injected, and its return value is the implementation of that interface. All in all, @Module and @Provides provide a dependency graph.

In the following code, for the interface OrderRepository, we choose to use OrderRepositoryImpl as its implementation.

import dagger.Module
import dagger.Provides

@Module
class OrderModule {
    @Provides
    fun orderRepository(): OrderRepository = OrderRepositoryImpl()

    @Provides
    fun orderManager(repository: OrderRepository): OrderManager = OrderManagerImpl(repository)
}

@Component & @Inject

@Inject tells Dagger the target to inject. In the following code, we tell Dagger that we want to inject OrderManager.

class OrderFirstFragment : Fragment() {
    ...

    @Inject
    lateinit var orderManager: OrderManager

    ...
}

We also declare @Component interface to tell Dagger where to inject and which module to use for instantiating the implementation. In the following code, we declare the interface OrderComponent. Note that it must be an interface, because Dagger will automatically generate class DaggerOrderComponent afterwards.

import dagger.Component

@Component(modules = [OrderModule::class])
interface OrderComponent {
    fun inject(fragment: OrderFirstFragment)
}

Using Dagger to Inject Objects

So far, we have declared @Module and @Component, and we have also specified to inject OrderManager in OrderFirstFragment. But these only tell Dagger which objects to inject, where to inject, and how to instantiate the implementations. Dagger only knows this information, we have to tell it when to inject.

We declared an interface OrderComponent, Dagger will automatically generate class DaggerOrderComponent. Usually we instantiate the @Component in Application, so that all Activity in the app can access. But in order to simplify the code, in the example, we instantiate the OrderComponent instance in OrderActivity.

class OrderActivity : AppCompatActivity() {
    ...

    lateinit var orderComponent: OrderComponent

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        orderComponent = DaggerOrderComponent.create()

        ...
    }
    ...
}

Then in OrdreFirstFragment, we get OrderComponent, and the call inject() to perform the injection to OrderFirstFragment.

class OrderFirstFragment : Fragment() {
    ...

    @Inject
    lateinit var orderManager: OrderManager

    override fun onAttach(context: Context) {
        activity?.let {
            (it as OrderActivity).orderComponent.inject(this)
            Log.d(javaClass.canonicalName, "orderManager: $orderManager")
        }
        super.onAttach(context)
    }

    ...
}

The basic usage of Dagger is roughly as above. Compared with the manual method, it is actually similar.

Scope Management: @Singleton

In the previous section, we found that in OrderFirstFragment, the instances of OrderManager injected each time are different entities. When we want OrderManager to have only one instance in the app, we can use @Singleton to tell Dagger to instantiate the OrderManager only once.

Open OrderModule, add @Singleton to orderRepository() and orderManager(). Because we hope that in the app, both OrderRepository and OrderManager need only one instance.

import dagger.Module
import dagger.Provides
import javax.inject.Singleton

@Module
class OrderModule {
    @Singleton
    @Provides
    fun orderRepository(): OrderRepository = OrderRepositoryImpl()

    @Singleton
    @Provides
    fun orderManager(repository: OrderRepository): OrderManager = OrderManagerImpl(repository)
}

We must also add @Singleton to OrderComponent as well.

import dagger.Component
import javax.inject.Singleton

@Singleton
@Component(modules = [OrderModule::class])
interface OrderComponent {
    fun inject(fragment: OrderFirstFragment)

    fun inject(fragment: OrderSecondFragment)
}

That’s it. You will find that the OrderManager injected into OrderFirstFragment by Dagger is the same entity every time .

@Binds

In the above example, we put all the information to be provided to Dagger in @Module and @Component. Dagger provides another way to provide these information, that is @Binds . @Binds will help us automatically generate the code for instantiating implementation in @Module.

Below we will use another example Product to show how to use @Binds. The structure of the example Product is almost the same as the example Order.

data class Product(
    val name: String,
    val price: Double,
)
interface ProductRepository {
    fun findAll(): List<Product>
}
interface ProductManager {
    fun getAll(): List<Product>
}

The difference with OrderRepositoryImpl is that we must add @Inject before the constructor of ProductRepositoryImpl, even if the constructor does not have any parameters, we must add @Inejct. If you want ProductRepositoryImpl to have only one entity in the app, add @Singleton to it.

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ProductRepositoryImpl @Inject constructor() : ProductRepository {
    override fun findAll(): List<Product> = listOf(
        Product("Chocolate", 1.0),
        Product("Jelly Bean", 0.2),
    )
}

There is a parameter of ProductRepository in the constructor of ProductManagerImpl, and then Dagger will automatically pass the parameter ProductRepository when instantiate ProductManagerImpl.

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ProductManagerImpl @Inject constructor(private val repository: ProductRepository) :
    ProductManager {
    override fun getAll(): List<Product> = repository.findAll()
}

Now we will declare @Module as the following code. As you can see, ProductModule is quite clean compared to OrderModule. It is worth noting that class ProductModule must be abstract. Because then Dagger will help us generate the code for all methods.

In ProductModule, we declare two methods, one of which is productManager(). We add @Binds to it, telling Dagger that this method uses the type ProductManagerImpl of its parameter as the implementation of the return type ProductManager.

import dagger.Binds
import dagger.Module

@Module
abstract class ProductModule {
    @Binds
    abstract fun productRepository(productRepository: ProductRepositoryImpl): ProductRepository

    @Binds
    abstract fun productManager(productManager: ProductManagerImpl): ProductManager
}

Then, we also have to declare @Component.

import dagger.Component
import javax.inject.Singleton

@Singleton
@Component(modules = [ProductModule::class])
interface ProductComponent {
    fun inject(fragment: ProductFirstFragment)

    fun inject(fragment: ProductSecondFragment)
}

The usage of the example Product is the same as the example Order.

class ProductActivity : AppCompatActivity() {
    ...

    lateinit var productComponent: ProductComponent

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        productComponent = DaggerProductComponent.create()
        ...
    }
    ...
}
class ProductFirstFragment : Fragment() {
    ...

    @Inject
    lateinit var productManager: ProductManager

    override fun onAttach(context: Context) {
        activity?.let {
            (it as ProductActivity).productComponent.inject(this)
            Log.d(javaClass.canonicalName, "productManager: $productManager")
        }
        super.onAttach(context)
    }
    ...
}

We see that when using @Binds, we have to add @Inject and @Singleton to ProductManagerImpl. In other words, ProductManagerImpl must know Dagger. If ProductManagerImpl is in another module, using @Binds is a bad practice.

Conclusion

When your project becomes larger, you will need to dependency injection to maintain the structure and cleanliness of the code. In simple cases, dependency injection may be implemented manually. However, when there are many objects that need to be injected, perhaps Dagger is a better choice.

Reference

  1. Dagger basics, Android Developers
  2. Using Dagger in Android apps, Android Developers
  3. Dagger 2 Tutorial For Android: Advanced, raywenderlich.com
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like