-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Comments
Edit: This resolved in Dagger 2.25+ by 646e033 If you have injected properties (as "fields"), qualifiers must have 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 |
Edit: Objects are handled in Dagger 2.25+. Companion objects are handled in Dagger 2.26+. Static provides can be achieved via top-level @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:
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 |
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 The fourth function is also valid, but now 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. |
An expression body doesn't preclude the use of a return type.
…On Mon, Oct 16, 2017, 5:10 PM Zac Sweers ***@***.***> wrote:
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 take write in one of two ways
@provides fun provideDiskCache() = DiskCache()
@provides fun provideDiskCache(): Cache {
return DiskCache()
}
The first function is valid, but DiskCache is what's on the DI graph
there because that's the inferred return type.
The second function is also valid, but now Cache is what's on the graph
and DiskCache is just an implementation detail.
The IDE will try to suggest inlining the second one. You can do so, but be
mindful of potentially changing return types.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#900 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAEEEd-hmcPrT3JVf03HxMXNZWvOSOJuks5ss8ZQgaJpZM4P5yKC>
.
|
Good point, tweaked the wording to make it clear that it's only if you drop the return type and added more examples |
|
What a great thread! I was fighting this fight this last weekend. Here is the syntax for injection that's both qualified and nullable:
|
@tasomaniac #807 has also cost me quite some time to debug. @Inject lateinit var foo: Set<@JvmSuppressWildcards Foo> (taken from https://stackoverflow.com/a/43149382/2037482) |
As an add-on from @heinrichreimer, the same thing is required when injecting functions that have parameters:
|
With Kotlin 1.2 we can use array literals in annotations, ditching
|
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")
}
}
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. |
You need to add @JvmSuppressWildcards at the injection site to ensure the
signature matches.
…On Mon, Jan 22, 2018 at 7:55 PM Miguel Vargas ***@***.***> wrote:
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
@Providesfun 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.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#900 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAEEEWWIjIzLpOQBN_IbTjP239JfGh-Vks5tNS4LgaJpZM4P5yKC>
.
|
I understand that this issue about "best practices", but on Reddit AMA @ronshapiro mentioned that this issue the place to collect kotlin-specific features.
|
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. |
The method injection works well on Kotlin setter.
If you need the qualifier, must write the custom setter.
|
@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 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 |
The |
@ronshapiro same question -- does that same behavior occur with Kotlin's "static" implementations? In the case of top-level Kotlin |
@sophiataskova See the discussion I had with Jeff Bowman on this topic, maybe you'll find it useful. |
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 🎉 |
Should I take it that Kotlin does not offer any of the advantages of Ron says that |
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 |
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. |
Recording a thought from Droidcon: someone mentioned that Could we support an annotation on the class, detect that it's a kotlin class, and treat the sole constructor as having |
I think you should ignore that person!
The last point isn't tied to any language and should probably be considered
separately.
…On Wed, Aug 29, 2018, 7:22 PM Ron Shapiro ***@***.***> wrote:
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).
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#900 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAEEEfuOXjQQGhJ7dwT0Yf1rX8nHiXAQks5uVyIbgaJpZM4P5yKC>
.
|
@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 |
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. |
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. |
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 |
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 |
Can we expect at some point to be able to add @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 @Module
class MyFunctionModule {
@Binds
fun provideMyFunction(
dependency1: Dependency1,
...
dependencyN: DependencyN
): () -> Something = makeMyFunction(
dependency1: Dependency1,
...
dependencyN: DependencyN
)
} We can of course move implementation of The added benefit of being able to add 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 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. |
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. |
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. |
What you're looking for is something akin to Guice's Fwiw, the way Dagger works, if it finds that it needs to inject a type 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. |
That definitely makes sense. Thanks @tbroyer for detailed explanation. Btw, I feel like this can already be possible with Hilt by having |
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 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. |
@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 |
@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! |
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.
The text was updated successfully, but these errors were encountered: