Dependency Injection in Kotlin — How to setup Dagger2
If you want to use Dagger as your Dependency Injection (DI) framework with Kotlin and don't know how to setup it, you are in the right place.
Configure Kotlin to use Dagger
Dagger is a fully static, compile-time dependency injection framework for Java, Kotlin, and Android. It is an adaptation of an earlier version created by Square and now maintained by Google. [1]
We are going to build an example of digital movies library application that has two main functionalities: find all movies and find movies based on the year. This application will be as simple as possible to present only the Dagger features.
As we can see in the previous diagram, Movie
has only two attributes title
and year
. Our application will have only one service class, MoviesList
who has Movies
which is an interface that returns all library movies.
We could have had a lot of more implementation of Movies
focused on different aspects such as type of movie (gender), storage technology (database, xml file, json file, Restful API, etc) and others. However, for the sake of simplicity I chose to implement based on blockbuster movies.
Before jumping in the configuration, let's highlight the technologies used in our example:
- Kotlin 1.8 (JVM 19)
- Dagger 2.46.1
- Gradle 8.0.2 (Kotlin DSL)
Add Dependencies
To use Dagger library, we need to add it in dependencies on our build file build.gradle.tks
.
dependencies {
implementation("com.google.dagger:dagger:2.46.1")
kapt("com.google.dagger:dagger-compiler:2.46.1")
}
As Dagger is a fully-static and compile-time dependency injection framework, it relies on annotations to generate code that will be used in our application — this process is called annotation processor.
So, we need to setup our annotation processor. Kotlin has two plugins: Kotlin Symbol Processing (KSP) and Kotlin Annotation Processing Tool (KAPT). Kapt is in maintenance mode and documentation recommends to use KSP. However, Dagger does not support KSP yet [2]. Then, we have to configure Kapt to generate code in compile-time.
Create Movie data class and Movies interface
Our Movie
class is as simple as possible and has only two attributes: title
and year
. Meanwhile our interface repository has only one method that returns all movies of our library.
data class Movie(val title: String, val year: Number)
interface Movies {
fun findAll(): List<Movie>
}
Implement Movies interface
Our Movies
implementation will return only blockbuster movies. As the interface will be provided by Dagger, we need to tell it how can it constructs the MoviesBlockbuster
implementation.
import javax.inject.Inject
class MoviesBlockbuster @Inject constructor(): Movies {
override fun findAll() = listOf(
Movie("John Wick 4: Baba Yaga", 2023),
Movie("KSI: In Real Life", 2023),
Movie("Assassin", 2023),
Movie("The Gray Man", 2022),
Movie("The Whale", 2022),
Movie("Hustle", 2022)
)
}
In order to let Dagger knows that it will construct MoviesBlockbuster
, we need to add @Inject
annotation. Othterwise, Kapt will throw a compile-time error while trying to instantiate an implementation of Movies
.
Create MoviesList service class
Our MoviesList
service class has two functionalities: the first one (findAll
) returns all movies of our library. To do so, service class calls Movies
to return all movies from its library. In this method MoviesList
acts just as a proxy.
The second one (findByYear
) returns movies from a specific year. In order to do, service class calls Movies
and performs filtering logic — to get only movies from a specific year — on the result of Movies
call.
import javax.inject.Inject
class MoviesList @Inject constructor (private val movies: Movies){
fun findAll() = movies.findAll()
fun findByYear(year: Number) = movies.findAll().filter { it.year == year }
}
However, it is not a concern or responsibility of MoviesList
to know how to instantiate its Movies
dependency. Dagger, as a DI framework, is the one responsible to do it. So we also need to add @Inject
annotation to let Dagger instantiate MoviesList
service.
Create Movies Module
Now, we are missing two pieces of our application: tells Dagger how to instantiate Movies
and create a class that provides MoviesList
service class. In this section, we will will see the first missing part.
In the previous sections, we created our Movies
interface, created MoviesBlockbuster
implementation and created our MoviesList
service class.
MoviesList
receives Movies
in its constructor, so we need to tell Dagger how to bind the interface with the implementation. In order to do that, we need to create a Dagger module and a method that returns the interface implementation.
As your Movies
implementation is quite simple and doesn't receive any parameters, we can use @Bind
in favor of @Provider
. To be able to use @Bind
the dependency needs to meet following criteria:
@Binds
needs to have just one method parameter which is the return type of the method or its subtype. To check more about differences between@Binds
and@Provider
take a look at Dagger Documentation.
import dagger.Binds
import dagger.Module
import javax.inject.Singleton
@Module
interface MoviesModule {
@Binds
@Singleton
fun bindMovies(moviesBlockbuster: MoviesBlockbuster): Movies
}
The snippet code above shows how to create a Dagger module. The name of the method could be any name. However, by Dagger's convention, we use bind
as prefix plus the name of the component that Dagger constructs.
Create MoviesApp
After telling Dagger how to create our dependencies, finally, we need to create an application that will provide our MoviesList
functionality.
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [MoviesModule::class])
interface MoviesApp {
fun moviesList(): MoviesList
}
The snippet above, declares our MoviesApp
as an interface, annotated by @Singleton
and @Component
. The first one tells Dagger that it will only have one instance of MoviesApp
. The second one, annotates an interface or abstract class for which a fully-formed, dependency-injected implementation is to be generated by Dagger from a set of modules
[3].
As Dagger is a fully static, compile-time dependency injection framework when we finish to setup, we need to run kaptKotlin
annotation processor task to generate Dagger classes. After running it, we can use MoviesApp
generated class.
fun main() {
val app = DaggerMoviesApp.builder().build()
val movies = app.moviesList()
movies.findByYear(2022).forEach { println(it) }
movies.findByYear(2023).forEach { println(it) }
}
Troubleshooting
If we don’t add @inject
to classes that will have dependencies injected by Dagger we can a missing binding usage problem:
> Task :kaptKotlin FAILED
/build/tmp/kapt3/stubs/main/MoviesApp.java:6: error: [Dagger/MissingBinding] MoviesBlockbuster cannot be provided without an @Inject constructor or an @Provides-annotated method.
public abstract interface MoviesApp {
^
Missing binding usage:
MoviesBlockbuster is injected at
MoviesModule.bindMovies(moviesBlockbuster)
Movies is injected at
MoviesList(movies)
MoviesList is requested at
MoviesApp.moviesList()
In the error above, we forgot to add @inject
to MoviesBlockbuster
constructor. To fix this problem, we need to add it and re-run kaptKotlin
task.
With just a few steps we have configured Dagger to be our DI framework and created a simple application that uses Dagger generated ccode class. If you liked this article and want to dive deep in Dagger's story, I would recommend you to watch "DAGGER 2 — A New Type of dependency injection" talk [4].