InsertKoinIO / koin-annotations

Koin Annotations - About Koin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform insert-koin.io
https://insert-koin.io
Apache License 2.0
123 stars 30 forks source link

Unintuitive behavior when using default parameters. #127

Open Jadarma opened 2 months ago

Jadarma commented 2 months ago

Unintuitive behavior for dependencies with default constructor arguments, as a consequence of #54 . Parameters with default values will never be injected even if candidates exist, but no warnings or errors are given. One can only discover this by observing runtime behavior.

To Reproduce Consider the following:

package com.example

@Factory
class Component(@InjectedParam val param: String = "default")

@Module
@ComponentScan("com.example")
class MyModule

fun main() {
    startKoin {
        modules(MyModule().module)
    }
    val default = getKoin().get<Component>()
    println(default.param) // default ✅

    val explicit = getKoin().get<Component> { parametersOf("explicit") }
    println(explicit.param) // default ❌
}

Expected behavior Constructor arguments should only use defaults when no dependency can be injected.

I would expect this:

@Factory
class Component(@InjectedParam val param: String = "default")

To be the equivalent of this:

factory<Component> { parmeterHolder -> Component(param = parameterHolder.getOrNull() ?: "default" }

Alternative Compromise The problem with the expected behavior above is that it could prove a bit challenging to generate the code for defaults that are not literals, but complex functions that depend on other params, etc.

As an alternative in case the codegen for this would be unfeasible, I would rather have it a compile error to provide default values to parameters of classes annotated by Koin annotations, since those imply the user intent of being injected, but will be ignored by the current compiler implementation due to the default.

In order to still allow the use-case of #54, we could instead use a marker annotation like @NoInject, which then requires that the param have a default value, such that it can be instantiated by the module (but would function as normal when creating the instance by hand), and fail when a default is provided on anything else.

// OK
@Single class FooA(@NoInject baz: String = "default")

// Error, no default provided, the generated module code cannot instantiate.
@Single class FooB(@NoInject baz: String)

// Error, default will override, and annotations on `baz` are useless!
@Single class FooC(                  baz: String = "default") 
@Single class FooD(@InjectedParam    baz: String = "default")
@Single class FooE(@Named("bazinga") baz: String = "default")

In this case this:

@Single class Foo(val bar: Bar, @NoInject val baz: Baz = SomeBazImpl())

Would be the equivalent of this, and the Kotlin language will take care of the default argument instead:

single<Foo> { Foo(bar = get()) }

Koin project used and used version: io.insert-koin:koin-bom:3.5.1 io.insert-koin:koin-annotations-bom:1.3.0