Dagger in Kotlin - Gotchas and Optimizations
Dagger optimizations come with no cost! Add them and follow best practices
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:
allprojects {
afterEvaluate {
extensions.findByName('kapt')?.arguments {
arg("dagger.formatGeneratedSource", "disabled")
arg("dagger.gradle.incremental", "enabled")
}
}
}
Alternatively, if you use Kotlin DSL script files, include them like this in the build.gradle.kts
file of the modules that use Dagger:
kapt {
arguments {
arg("dagger.formatGeneratedSource", "disabled")
arg("dagger.gradle.incremental", "enabled")
}
}
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.
Update 10/28/19: This was fixed in Dagger v2.25. If you use this version, you can just type what wasn’t working before:
@Inject @MinimumBalance lateinit var minimumBalance: BigDecimal
// FIXED: @MinimumBalance is NOT ignored!
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.
@Module
object NetworkModule {
@JvmStatic
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}
}
In case you need an abstract method, you’ll need to add the @JvmStatic
method to a companion object and annotate it with @Module
too.
@Module
abstract class NetworkModule {
@Binds abstract fun provideService(retrofitService: RetrofitService): Service
@Module
companion object {
@JvmStatic
@Provides
fun provideOkHttpClient(): OkHttpClient {
return return OkHttpClient.Builder().build()
}
}
}
Alternatively, you can extract the object module out and include it in the abstract one:
@Module(includes = [OkHttpClientModule::java])
abstract class NetworkModule {
@Binds abstract fun provideService(retrofitService: RetrofitService): Service
}
@Module
object OkHttpClientModule {
@JvmStatic
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}
}
Update 10/28/19: With Dagger v2.25.2, you don’t need to tag the @Provides
function with @JvmStatic
. Dagger will understand it properly.
Injecting Generics
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 causes problems with Dagger because it expects an exact (aka invariant) type match. Using @JvmSuppressWildcards
will ensure that Dagger sees the type without wildcards.
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:
@Provides
fun provideNetworkPrinter() = NetworkPrinter()
@Provides
fun provideNetworkPrinter(): NetworkPrinter = NetworkPrinter()
@Provides
fun provideNetworkPrinter(): NetworkPrinter {
return NetworkPrinter()
}
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:
@Provides
// configures a `Printer`
fun providePrinter(): Printer = NetworkPrinter()
@Provides
// configures a `NetworkPrinter`, not a plain `Printer`!
fun providePrinter() = NetworkPrinter()
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.