Dagger is a popular Dependency Injection framework commonly used in Android. It provides fully static and compile-time dependencies addressing many of the development and performance issues that have reflection-based solutions.
This month, a new tutorial was released to help you better understand how it works. This article focuses on using Dagger with Kotlin, including best practices to optimize your build time and gotchas you might encounter.
Dagger is implemented using Java’s annotations model and annotations in Kotlin are not always directly parallel with how equivalent Java code would be written. This post will highlight areas where they differ and how you can use Dagger with Kotlin without having a headache.
This post was inspired by some of the suggestions in this Dagger issue that goes through best practices and pain points of Dagger in Kotlin. Thanks to all of the contributors that commented there!
kapt build improvements
To improve your build time, Dagger added support for gradle’s incremental annotation processing in v2.18! This is enabled by default in Dagger v2.24. In case you’re using a lower version, you need to add a few lines of code (as shown below) if you want to benefit from it.
Also, you can tell Dagger not to format the generated code. This option was added in Dagger v2.18 and it’s the default behavior (doesn’t generate formatted code) in v2.23. If you’re using a lower version, disable code formatting to improve your build time (see code below).
Include these compiler arguments in your
build.gradle file to make Dagger more performant at build time:
Alternatively, if you use Kotlin DSL script files, include them like this in the
build.gradle.kts file of the modules that use Dagger:
Qualifiers for field attributes
When an annotation is placed on a property in Kotlin, it’s not clear whether Java will see that annotation on the field of the property or the method for that property. Setting the
field: prefix on the annotation ensures that the qualifier ends up in the right place (See documentation for more details).
✅ The way to apply qualifiers on an injected field is:
@Inject @field:MinimumBalance lateinit var minimumBalance: BigDecimal
❌ As opposed to:
@Inject @MinimumBalance lateinit var minimumBalance: BigDecimal
// @MinimumBalance is ignored!
Forgetting to add
field: could lead to injecting the wrong object if there’s an unqualified instance of that type available in the Dagger graph.
Static @Provides functions optimization
Dagger’s generated code will be more performant if
@Provides methods are
static. To achieve this in Kotlin, use a Kotlin
object instead of a
class and annotate your methods with
@JvmStatic. This is a best practice that you should follow as much as possible.
In case you need an abstract method, you’ll need to add the
@JvmStatic method to a companion object and annotate it with
Alternatively, you can extract the object module out and include it in the abstract one:
Kotlin compiles generics with wildcards to make Kotlin APIs work with Java. These are generated when a type appears as a parameter (more info here) or as fields. For example, a Kotlin
List<Foo> parameter shows up as
List<? super Foo> in Java.
This is a common issue when you inject collections using Dagger’s multibinding feature, for example:
class MyVMFactory @Inject constructor(
private val vmMap: Map<String, @JvmSuppressWildcards Provider<ViewModel>>
Inline method bodies
Dagger determines the types that are configured by
@Provides methods by inspecting the return type. Specifying the return type in Kotlin functions is optional and even the IDE sometimes encourages you to refactor your code to have inline method bodies that hide the return type declaration.
This can lead to bugs if the inferred type is different from the one you meant. Let’s see some examples:
If you want to add a specific type to the graph, inlining works as expected. See the different ways to do the same in Kotlin:
If you want to provide an implementation of an interface, then you must explicitly specify the return type. Not doing it can lead to problems and bugs:
Dagger mostly works with Kotlin out of the box. However, you have to watch out for a few things just to make sure you’re doing what you really mean to do:
@field: for qualifiers on field attributes, inline method bodies, and
@JvmSuppressWildcards when injecting collections.
Dagger optimizations come with no cost, add them and follow best practices to improve your build time: enabling incremental annotation processing, disabling formatting and using static
@Provides methods in your Dagger modules.