Dagger 2 - Constructor Dependency Injection in Kotlin

Updated dimitrilc 2 Tallied Votes 1K Views Share

Introduction

In Android development, the current recommended library for Dependency Injection is Hilt. The most obvious benefits of using Hilt are reduced boilerplate code and lifecycle-aware components(and their associated annotations).

Hilt is a great library, but it is not perfect. Hilt is built on top of Dagger 2, so the terminologies and concepts are hard to understand for developers who did not use Dagger before being introduced to Hilt.

This tutorial introduces Dagger 2 in its original form using Kotlin to make it easy for Kotlin-only developers to follow. The main feature that we are going to look at in this tutorial is constructor dependency injection using Dagger 2.

Goals

At the end of this tutorial, you would have learned:

  1. How to use Dagger constructor injection.

Prerequisite Knowledge

  1. What dependency injection is.
  2. Basic Kotlin.

Tools Required

  1. IntelliJ Community Edition.

Project Setup

First we need to set up our Kotlin project:

  1. In IntelliJ, go to File > New > Project.
  2. In the New Project wizard:
    a. Select Kotlin on the left hand side.
    b. Use Dagger2Practice as project name(optional).
    c. You can leave the Location as default if you wish.
    d. Project Template is Application.
    e. Build System is Gradle Kotlin.
    f. Project JDK is set at Java 11. I find this version to be most stable when working with Kotlin.
    g. Group ID is com.example.
    h. Artifact ID is Dagger2Practice.
    i. Version is 1.0-SNAPSHOT.
    j. This is how the project setting looks like:

Untitled.png

  1. Click Next.

  2. Leave the next screen as default and click Finish. Wait a few seconds for the project to build.

  3. Copy and paste this exact same gradle configuration below into your build.gradle.kts file.

     import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
    
     plugins {
        kotlin("jvm") version "1.5.30"
        kotlin("kapt") version "1.5.30"
     }
    
     group = "com.example"
     version = "1.0-SNAPSHOT"
    
     repositories {
        mavenCentral()
        google()
     }
    
     dependencies {
        kapt("com.google.dagger:dagger-compiler:2.38.1")
        implementation("com.google.dagger:dagger:2.38.1")
        testImplementation("org.jetbrains.kotlin:kotlin-test:1.5.21")
     }
    
     tasks.test {
        useJUnitPlatform()
     }
    
     tasks.withType<KotlinCompile>() {
        kotlinOptions.jvmTarget = "1.8"
     }
  4. In your IDE, go to Build -> Build Project. This will bring in necessary libraries and annotation processing for your project.

  5. Under the kotlin directory, create the packages below. Later on, we will put Dagger boilerplate code in them.

     com.example.component
     com.example.module
  6. Create a new file Entry.kt under the package com.example. This is where your main function lives, so create a main() function here.

  7. To be able to use DI, we are going to need an object that depends on some other objects. Under the package com.example, create a data class called Store. Store will depend on two other classes that we will tell Dagger to inject them at runtime. Copy the source code below into Store.kt. Don’t worry if the code does not compile for now.

     package com.example;
    
     import javax.inject.Inject
    
     data class Store
     @Inject constructor
        (val manager: Manager,
         val employee: Employee)
  8. Under the com.example package, create a Manager interface. Copy the code below into Manager.kt.

     package com.example
    
     interface Manager
  9. Also under com.example, create a data class called Employee. This data class has one string field to store the employee name.

     package com.example
    
     data class Employee(val name: String)

Concept Overview

For Dagger to create a Store object at runtime, we have to configure the whole dependency graph, teaching Dagger how to construct objects with configurable constructors or instances of interfaces.

dager2.png

At the top of a dependency graph is a Component. A Component is an interface with an abstract function returning the type of the instance we want (Store in this case). We can provide Modules to a Component to help it complete its dependency graph.

A Module here is not a gradle or Java module. It is a class/interface that contains functions providing the dependency objects at runtime. Have you noticed that Manager is an interface and Employee is a configurable class? Dagger requires the developer to teach it how to provide instances for both.

Employee Module

Let us satisfy Dagger’s requirements one by one, starting with an argument of type Employee for the Store class.

Under com.example.module, create an EmployeeModule abstract class.

    package com.example.module

    import com.example.Employee
    import dagger.Module
    import dagger.Provides

    @Module //1
    abstract class EmployeeModule {

       companion object { //2
           @Provides //3
           fun provideEmployee() = Employee("Anna") //4
       }

    }

Here are the explanations for the code above:

  1. The @Module annotation at line 1 marks the class as a Dagger module.
  2. We put the function that provides an Employee inside a companion object to make static and Dagger does not have to instantiate EmployeeModule at runtime.
  3. The @Provides annotation marks the function as a function that provides a dependency object instance.
  4. The function provideEmployee() returns an Employee of the name Anna at runtime. By convention, the method name for a @Provides annotation should start with “provide”.

Manager Module

Now that we are finished with the EmployeeModule, we have to teach Dagger how to provide an instance of Manager next.

Let’s not use the @Provides annotation again. Dagger has another annotation that is @Binds. @Binds works differently to @Provides. A @Binds function is abstract and “binds” the function argument as the return value.

Under com.example.module, create a ManagerModule interface.

    package com.example.module

    import com.example.Manager
    import com.example.ManagerImpl
    import dagger.Binds
    import dagger.Module

    @Module //1
    interface ManagerModule { //2

       @Binds //3
       fun bindManager(managerImpl: ManagerImpl): Manager //4

    }

As you can see, the function bindManager takes a ManagerImpl argument (that we have not created yet). This ManagerImpl type must be on the dependency graph and it must implement Manager. Under com.example, create a ManagerImpl class that implements Manager.

    package com.example

    import javax.inject.Inject

    class ManagerImpl @Inject constructor(): Manager

Notice that the constructor of ManagerImpl is an empty no-args constructor marked with @Inject. Dagger knows that this class can be instantiated by calling the empty constructor, which will make it easy for it to pass into the bindManager() function defined previously.

Store Component

Now that we have configured both of the Modules in the dependency graph, the final step is to declare the StoreComponent.

Under com.example.component, create an interface called StoreComponent.

    package com.example.component

    import com.example.Store
    import com.example.module.EmployeeModule
    import com.example.module.ManagerModule
    import dagger.Component

    @Component(modules = [ManagerModule::class, EmployeeModule::class]) //1
    interface StoreComponent { //2
       fun store(): Store //3
    }  

A Component needs to be annotated with @Component. This annotation takes a list of class objects for the Modules. By passing in the module classes, we declare that these modules can be used to generate an implementation of StoreComponent. The implementation class will be prefixed with Dagger and the rest of the class name is the same as the interface name.

Go back to our main function (in Entry.kt), and copy the code below into your IDE.

    import com.example.Store
    import com.example.component.DaggerStoreComponent

    fun main(){
       val store: Store = DaggerStoreComponent.create().store()
       println(store.employee)
       println(store.manager)
    }

To get an instance of Store, we simply call the Dagger-generated implementation DaggerStoreComponent -> create() -> store().

store() is the abstract function that we declared earlier.

You might have to go to Rebuild the project for the generated class to show up and for your code to compile.

Solution Code

The full project code can be found here https://github.com/dmitrilc/DaniWebDagger2Constructor/tree/master

Summary

If you look at the final source code folder, you might have noticed that we created quite a lot of boilerplate code just for Dagger: 2 modules and one component. Dagger 2 is somewhat complex to set up, but it is worth it for large projects.