Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ronshapiro opened this issue Oct 15, 2017 · 53 comments
Open

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

ronshapiro opened this issue Oct 15, 2017 · 53 comments

Comments

@ronshapiro
Copy link

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.

@google google deleted a comment from bejibx Oct 16, 2017
@ZacSweers
Copy link

ZacSweers commented Oct 16, 2017

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
Copy link

ZacSweers commented Oct 16, 2017

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
Copy link

ZacSweers commented Oct 16, 2017

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
Copy link

JakeWharton commented Oct 16, 2017 via email

@ZacSweers
Copy link

ZacSweers commented Oct 16, 2017

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

@tasomaniac
Copy link

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

@jhowens89
Copy link

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
Copy link

@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
Copy link

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
Copy link

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

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

@guelo
Copy link

guelo commented Jan 23, 2018

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
Copy link

JakeWharton commented Jan 23, 2018 via email

@gildor
Copy link

gildor commented Feb 8, 2018

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.

@ronshapiro
Copy link
Author

It would probably be good to make sure that no binding method used default parameters. It wouldn't make sense, and it definitely would be misleading.

@tobeylin
Copy link

The method injection works well on Kotlin setter.

@set: Inject
lateinit var booleanProvider: Provider<Boolean>

If you need the qualifier, must write the custom setter.

var booleanProvider: Provider<Boolean>? = null
        @Inject set(@MyQualifier value){
            field = value
}

@sophiataskova
Copy link

sophiataskova commented Aug 1, 2018

@hzsweers re: your comment on static provides methods in Kotlin, does the Kotlin implementation offer the same performance wins as "true" Java static methods? Does it provide any performance wins? What's your main reason for making your modules this way?

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
Copy link

ZacSweers commented Aug 1, 2018

static provides allow you to make your modules truly stateless and allows you to simplify plumbing (no instance wiring required). The biggest win there is basically architecture. The "performance" wins of invokestatic always seemed kind of a reach. 25% of something measured in nanoseconds on a non-hotpath seems a nice-to-have rather than a strong motivating factor.

@ronshapiro
Copy link
Author

The invokestatic might not be the true benefit - not needing the module instance allows all static provides to get horizontally merged, eliminating the class loading of all of the module classes.

@sophiataskova
Copy link

sophiataskova commented Aug 1, 2018

@ronshapiro same question -- does that same behavior occur with Kotlin's "static" implementations? In the case of top-level Kotlin object, an instance of that class still gets created; in the case of companion object, the "static" methods are only accessible through an instance of the companion object. (I am learning both Kotlin and Dagger right now so please correct me if anything I said was false)

@arekolek
Copy link

arekolek commented Aug 2, 2018

@sophiataskova See the discussion I had with Jeff Bowman on this topic, maybe you'll find it useful.

@tasomaniac
Copy link

My 2 cents:

If somebody put a gun to your head to do 100% Kotlin, sure this is a solution. But I really prefer Java Modules to this. It is totally fine for me to 1 or 2 Java abstract classes in my feature. No inner classes, single flat file of provisions. Much simpler 🎉

@sophiataskova
Copy link

Should I take it that Kotlin does not offer any of the advantages of static @Provides that you get in Java?

Ron says that not needing the module instance allows all static provides to get horizontally merged, eliminating the class loading of all of the module classes but both Kotlin implementations offered here seem to still involve class loading for each module. Again, I kinda hope I'm wrong, and can make my app faster without converting to Java.

@JakeWharton
Copy link

First of all, this speed benefit will be extraordinarily minor. If you are looking to make your app faster then use a profiler. You will find 100 easier optimization targets.

Beyond that, don't use companion object for modules. Use object. In that case the instance will be unused and its initialization code will be removed by R8 and the methods will be truly static and can also be inlined just like Java.

@tasomaniac
Copy link

It is unfortunately not possible to have abstract methods in object. You would need 2 modules. I try not to mix abstract and static provisions anyways. But if you need to, object is not a solution. This JvmStatic trick allows you to have them in 1 module.

@ronshapiro
Copy link
Author

Recording a thought from Droidcon: someone mentioned that @Inject on constructors is awkward in Kotlin because the constructor is often implicit via the properties list. Using the constructor keyword is not idiomatic.

Could we support an annotation on the class, detect that it's a kotlin class, and treat the sole constructor as having @Inject? Or could we have an annotation in modules that adds a binding to a type's sole constructor as if it had @Inject (which may also make adding bindings for code that you don't own easier).

@JakeWharton
Copy link

JakeWharton commented Aug 29, 2018 via email

@ZacSweers
Copy link

@ronshapiro @gildor Another thought on default parameters: this could allow for otherwise "missing" dependencies when composing modules. Consider this:

class HttpModule {
  @Provides
  fun provideHttpClient(cache: Cache? = Cache.inMemoryCache()): HttpClient {
    return HttpClient(cache)
  }
}

@Module(includes = [HttpModule::class]
class TwitterModule {
  @Provides
  fun provideTwitterClient(httpClient: HttpClient): TwitterClient {
    return TwitterClient(httpClient)
  }
}

//
// TwitterModule could be used with any of the below AppModule variants
//

// Cache is defined and not null, HttpClient will end up with RealCache
@Module(includes = [TwitterModule::class]
class ProductionAppModule {
  @Provides fun provideCache(): Cache {
    return RealCache()
  }
}

// No cache is defined, HttpClient will end up with default inMemoryCache()
@Module(includes = [TwitterModule::class]
class IntegrationTestAppModule {
  
}

// Cache is defined but null, HttpClient will end up with no cache
@Module(includes = [TwitterModule::class]
class ConstrainedEnvironmentAppModule {
  @Provides fun provideCache(): Cache? = null
}

You can model absence and nullability in Kotlin at least. I'm not familiar enough with dagger to say off the top of my head if it differentiates between null and absent, but I'd assume it does with Optional. Then the above just boils down to invoking the defaults method and indicating if the cache parameter is present, rather than modeling with something like Optional. Invoking the synthetic defaults method would require reflection with today's build tooling, but could be achieved with bytecode generation once gradle and kapt properly support Filer-generated bytecode. The above approach is definitely advanced usage, but I think a really powerful affordance of the language. This can also be done with constructors for constructor injection.

@tasomaniac
Copy link

In a Dagger component, an instance is only ever available once. You don't have 2 different ways of injecting the same instance. If I'm not wrong, Kotlin default value support would introduce that. That means, it would not be clear which instance is injected in a given graph.

@gmale
Copy link

gmale commented Dec 14, 2019

Should these best practices be summarized into a document, perhaps as part of the initiative to improve Dagger on Android? That might allow this issue to be closed, rather than remain open indefinitely.

@ZacSweers
Copy link

The purpose of this issue is to be a living doc, I don’t think moving it into a frozen documentation that the community can’t contribute to is good

@kvithayathil
Copy link

Definitely like the idea of keeping this issue open for all contributors. It would be great to transpose of these best practises somewhere indexable/searchable

@dmitry-ryadnenko-izettle

Can we expect at some point to be able to add @Inject annotations to top-level factory functions like we can do with class constructors? Example.

@Inject
fun makeMyDependency(): MyDependency = object : MyDependency {

}

So, in Dagger, you can tell Dagger how to resolve a dependency by dropping an @Inject annotation on the class constructor.

class MyDependencyImpl @Inject constructor() : MyDependency {
    
}

What I'm missing really badly is a way to do the same thing but with top-level factory functions.

@Inject
fun makeMyDependency(): MyDependency = object : MyDependency {

}

One of the most important scenarios where I need this is to be able to tell Dagger how to provide a function.

@Inject
fun makeMyFunction(): () -> Something = { /* implement function */ }

Functions doesn't have constructors, so when using a factory function like the one above there is no way to benefit from boilerplate generation that we have with @Inject in class constructors. So when providing functions we always have to write boilerplate like this.

@Module
class MyFunctionModule {
    @Binds
    fun provideMyFunction(
            dependency1: Dependency1,
            ...
            dependencyN: DependencyN
    ): () -> Something = makeMyFunction(
            dependency1: Dependency1,
            ...
            dependencyN: DependencyN
    )
}

We can of course move implementation of makeMyFunction() to MyFunctionModule but then we introduce strong coupling of our DI agnostic code with Dagger. makeMyFunction() implementation can hold lets say an important business logic and it would be nice to not not mix this function with DI details. Adding @Inject annotation to it would still be "mixing with DI details" but to a much lesser extent than surrounding it with a module class.

The added benefit of being able to add @Inject to top level functions is that instead of doing something like this.

class MyDependencyImpl @Inject constructor() : MyDependency {
    
}

@Module
interface MyDependencyModule {
    @Binds
    fun provideMyDependency(myDependency: MyDependencyImpl): MyDependency
}

We can now do it like this instead.

@Inject
fun makeMyDependency(): MyDependency = object : MyDependency {

}

So, there is no longer need to come up with a name for the implementation class and there is no longer need for a module with @Binds annotations.

Basically the code bellow is no longer needed.

@Module
interface MyDependencyModule {
    @Binds
    fun provideMyDependency(myDependencyImpl: MyDependencyImpl): MyDependency
}

And for us it's a pretty common scenario when we have to do something like this. I think it would be nice to be able to not write this boilerplate anymore.

@JakeWharton
Copy link

Those seem far more like module-less providers than injection points. I can argue semantics, but I actually don't think it's that useful of a distinction in practice.

I'm extremely skeptical of how well this would work in a general sense. As soon as you get into things like scoping annotations you need the ability to associate a function with the corresponding component–especially since the function could live entirely separately from the component definition.

@tasomaniac
Copy link

Isn't that the same with constructor injection. When putting @Inject annotation to a class constructor you don't really associate it with a component either. Scopes are done via putting a scope annotation at class level.

@tbroyer
Copy link

tbroyer commented Jun 19, 2020

What you're looking for is something akin to Guice's @ProvidedBy, but that annotation goes on the type being injected, which I don't think is possible here; or like CDI's @Produces.
In any case, that would be something extra to JSR 330, not @Inject; so you cannot achieve "DI agnostic code", at least in the sense that it could work with another JSR 330 implementation than Dagger.

Fwiw, the way Dagger works, if it finds that it needs to inject a type A, it will look into the component's modules whether there's a specific binding for it, and will fall back to A's @Inject-annotated constructor. It wouldn't be able to find your provider/factory method without somehow scanning the classpath (at runtime or compile-time, but still) for anything that would provide the expected type (before falling back to using its constructor). This is actually how CDI works by design, classpath scanning, autodiscovery, etc. the opposite of Dagger's mantra that things should be explicit (hey, it even requires you to annotate a zero-argument constructor with @Inject!)

You could probably have your own annotation and annotation processor (or Kotlin compiler plugin) to generate that module for you based on your annotated function (and possibly then a build plugin, like Hilt, for even fewer boilerplate). This is actually not much different from AutoFactory or AssistedInject, except specific to Kotlin.
Or maybe when Dagger will have native support for factories (#1825) this would be somehow included?

@tasomaniac
Copy link

That definitely makes sense. Thanks @tbroyer for detailed explanation.

Btw, I feel like this can already be possible with Hilt by having @file:InstallIn and @file:Module annotations. I've not tried myself.

@guelo
Copy link

guelo commented Sep 4, 2020

Injecting constructors that have default parameters doesn't work. The workarounds are to use the traditional Java telescoping constructor pattern and mark one of them with @Inject, or to put the default values into the Dagger graph.

An example that I keep running into is testing java.time stuff deterministically. I want to optionally pass in a Clock but I don't really want to get Dagger involved. I want to write it like this,

class UnderTest @Inject constructor (clock: Clock = Clock.systemDefaultZone()) {
  ///
}

But that doesn't work. @JvmOverloads doesn't help because it ends up tagging all the generated constructors with @Inject.

@ZacSweers
Copy link

@guelo that's more of a feature request. Java source code in general cannot leverage default parameter values. It's possible dagger could support this, but it's nontrivial as it would require both reflection at runtime and implicit handling similar to @BindsOptionalOf

@kaushalyap
Copy link

kaushalyap commented Feb 3, 2021

@JakeWharton , @ZacSweers Why not people develop compile time DI tool like Dagger 2 in Kotlin? (there are Koin and Kodein-DI which are service locators). Is it because project scope is big?

Thanks in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests