Skip to content

Kotlin+Dagger best practices/documentation/pain points #900

Open
@ronshapiro

Description

@ronshapiro

Opening this as a tracking bug for all kotlin related documentation that we should be add/best practices that we should call out to make using Dagger w/ Kotlin easier.

One example: How to achieve the effect of static @Provides in Kotlin.

Feel free to comment new ideas, but don't make "me too" or "i agree with XYZ" comments.

Activity

deleted a comment from bejibx on Oct 16, 2017
ZacSweers

ZacSweers commented on Oct 16, 2017

@ZacSweers

Edit: This resolved in Dagger 2.25+ by 646e033


If you have injected properties (as "fields"), qualifiers must have field: designation.

Good

@Inject @field:ForApi lateinit var gson: Gson

Bad

@Inject @ForApi lateinit var gson: Gson // @ForApi is ignored!

Forgetting this can lead to subtle bugs if an unqualified instance of that type also exists on the graph at that scope, as that's the one that will end up being injected. This would make a great lint as well

ZacSweers

ZacSweers commented on Oct 16, 2017

@ZacSweers

Edit: Objects are handled in Dagger 2.25+. Companion objects are handled in Dagger 2.26+.


Static provides can be achieved via @JvmStatic. There are two scenarios I see this come up:

top-level objects

@Module
object DataModule {
  @JvmStatic @Provides fun provideDiskCache() = DiskCache()
}

If you have an existing class module, things get a bit weirder

@Module
abstract class DataModule {
  @Binds abstract fun provideCache(diskCache: DiskCache): Cache

  @Module
  companion object {
    @JvmStatic @Provides fun provideDiskCache() = DiskCache()
  }
}

The way this works is as follows:

  • the companion object must also be annotated as @Module
  • under the hood, the kotlin compiler will duplicate those static provides methods into the DataModule class. Dagger will see those and treat them like regular static fields. Dagger will also see them in the companion object, but that "module" will get code gen from dagger but be marked as "unused". The IDE will mark this as such, as the provideDiskCache method will be marked as unused. You can tell IntelliJ to ignore this for annotations annotated with @Provides via quickfix

I sent Ron a braindump once of how dagger could better leverage this, i.e. no requirement for JvmStatic and just call through to the generated Companion class, but I think that's out of the scope of this issue :). I've been meaning to write up a more concrete proposal for how that would work.

ZacSweers

ZacSweers commented on Oct 16, 2017

@ZacSweers

Another gotcha is inline method bodies. Dagger relies heavily on return types to connect the dots. In kotlin, specifying the return type is optional sometimes when you can inline the method body. This can lead to confusing behavior if you're counting on implicit types, especially since the IDE will often try to coerce you into quickfixing them away

That is, you could write in one of four ways

// Option 1
@Provides fun provideDiskCache() = DiskCache()

// Option 2
@Provides fun provideDiskCache(): DiskCache = DiskCache()

// Option 3
@Provides fun provideDiskCache(): DiskCache {
  return DiskCache()
}

// Option 4
@Provides fun provideDiskCache(): Cache {
  return DiskCache()
}

// Option 5
@Provides fun provideDiskCache(): Cache = DiskCache()

The first function is valid, but DiskCache is what's on the DI graph there because that's the inferred return type. The first three functions are functionally identical.

The fourth function is also valid, but now Cache is what's on the graph and DiskCache is just an implementation detail. The fifth function is functionally identical to the fourth.

The IDE will try to suggest inlining the fourth one. You can do so, but be mindful of potentially changing return types if you also drop the explicit return type.

JakeWharton

JakeWharton commented on Oct 16, 2017

@JakeWharton
ZacSweers

ZacSweers commented on Oct 16, 2017

@ZacSweers

Good point, tweaked the wording to make it clear that it's only if you drop the return type and added more examples

tasomaniac

tasomaniac commented on Oct 16, 2017

@tasomaniac

@JvmSuppressWildcards is incredibly useful when you are injecting classes with generics. Can be handy when using multimap injection.

jhowens89

jhowens89 commented on Oct 17, 2017

@jhowens89

What a great thread! I was fighting this fight this last weekend. Here is the syntax for injection that's both qualified and nullable:

@field:[Inject ChildProvider] @JvmField var coordinatorProvider: CoordinatorProvider? = null

janheinrichmerker

janheinrichmerker commented on Oct 26, 2017

@janheinrichmerker

@tasomaniac #807 has also cost me quite some time to debug.
Doesn't look nice but at least it works:

@Inject lateinit var foo: Set<@JvmSuppressWildcards Foo>

(taken from https://stackoverflow.com/a/43149382/2037482)

charlesdurham

charlesdurham commented on Nov 27, 2017

@charlesdurham

As an add-on from @heinrichreimer, the same thing is required when injecting functions that have parameters:

... @Inject constructor(val functionalThing: @JvmSuppressWildcards(true) (String) -> String)
InvisibleGit

InvisibleGit commented on Dec 2, 2017

@InvisibleGit

With Kotlin 1.2 we can use array literals in annotations, ditching arrayOf() call in them

@Component(modules = [LibraryModule::class, ServicesModule::class])

guelo

guelo commented on Jan 23, 2018

@guelo

As far as I can tell Dagger is unable to inject Kotlin functions. This fails

@Module
class Module() {
  @Provides fun adder(): (Int, Int) -> Int = {x, y -> x + y}
}

class SomeClass {
  @Inject lateinit var adder:  (Int, Int) -> Int

  fun init() {
    component.inject(this)
    println("${adder(1, 2)} = 3")
  }
}

error: kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> cannot be provided without an @Provides- or @Produces-annotated method.

Theoretically you could provide a kotlin.jvm.functions.Function2 but I failed to make it work. Even with this

@Provides
fun adder(): kotlin.jvm.functions.Function2<Integer, Integer, Integer> {
  return object : kotlin.jvm.functions.Function2<Integer, Integer, Integer> {
    override fun invoke(p1: Integer, p2: Integer): Integer {
      return Integer(p1.toInt() + p2.toInt())
    }
  }
}

it still says a Function2 cannot be provided.

JakeWharton

JakeWharton commented on Jan 23, 2018

@JakeWharton
gildor

gildor commented on Feb 8, 2018

@gildor

I understand that this issue about "best practices", but on Reddit AMA @ronshapiro mentioned that this issue the place to collect kotlin-specific features.
There is a couple possible kotlin-specific features that require work with Kotlin Metadata, but would be very useful for Kotlin projects and make dagger usage even more pleasant

  1. Support typealias as an alternative for @Named and custom @Qualifier annotations. It's completely compiler feature, but you can get typealias name from @metadata annotation, so can use it to detect different qualifiers of the same class
  2. Support nullability as an alternative for Optional value. But I see some drawbacks: you cannot distinguish between non-initilized nullable and inilized one, but it's still can be a usefult and efficient replacement for Optional dependencies.
  3. Native support of lambda injection that doesn't require to use @JvmSuppressWildcards. This proposal can be extended to other generics, like for Lists and so on, but it not always required behavior, but in the case of lambdas I don't see any good reason do not consider any lambda (kotlin.jvm.functions.* interface) as generic without a wildcard. It will make lambda injection much less wordy. Also, works pretty well with typealias as qualifier.

65 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @JakeWharton@tbroyer@gildor@cgruber@matejdro

        Issue actions

          Kotlin+Dagger best practices/documentation/pain points · Issue #900 · google/dagger