kosi-libs / Kodein

Painless Kotlin Dependency Injection

Home Page:https://kosi-libs.org/kodein

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Binding by generic type parameter

nsragow opened this issue · comments

I can't figure out a way to bind a generic (non-reified) type parameter:

class Container<Type>(val obj: Type) {
    fun typeBinder(builder: Kodein.Builder) {
        builder.apply { 
            bind() from singleton { obj }
        }
    }
}

Is this possible/supported?

Currently I am using the following:

class Container<Type : Any>(val obj: Type, val klass: KClass<Type>) {
    fun typeBinder(builder: Kodein.Builder) {
        builder.apply {
            val s = Singleton(
                scope,
                contextType,
                TT(klass),
                null,
                true,
            ) {
                obj
            }
            bind() from s
        }
    }
}

Is this the expected usage or is there a bind interface that takes a KClass?

I still don't understand what you are trying to achieve here.

What do you need to bind?
What's your use case?

The exact use case is a super class for tests that binds an Config (subclass of AppConfig) component for each test:

abstract class TcTest<Config : AppConfig>(val klass: KClass<Config>) {
    abstract val baseModule: Kodein.Module
    lateinit var module: Kodein.Module
    lateinit var config: Config

    abstract fun envVariables(): Map<String, String>
    abstract fun config(overrides: Map<String, String>): Config

    @Before
    fun before() {
        val config = config(envVariables())
        this@TcTest.module = Kodein.Module("TcTestModule", allowSilentOverride = true) {
            importOnce(baseModule, allowOverride = true)
            val s = Singleton(scope, contextType, TT(klass), null, true) {
                config
            }
            bind() from s
            bind<AppConfig> with singleton { config }
        }
        this@TcTest.config = config
    }
}

As far as I know there is no way to get reified class types when they are passed as constructor parameters, and the bind function I typically use requires a reified type.

Ok, I think I got it.
Indeed, there is no function that facilitate the binding of a KClass.
Are you using an old version of Kodein? the TT(...) function has been rename a long time ago, by erased(...) .

But, I trully why you would need to do this, and I don't get why this bindings are needed

            val s = Singleton(scope, contextType, TT(klass), null, true) {
                config
            }
            bind() from s
            bind<AppConfig> with singleton { config }

maybe something like this would do the trick for your use case ?

bindSingleton<AppConfig> { config }
delegate<Config>().to<AppConfig()

Yeah we're using an older Kodein. The delegate call would not solve the issue because it still requires a reified type, which is not available because Config is a class generic type parameter. Unless there is a way to get a class generic reified.

I think maybe it would be worth supporting in the case where a class has a type it wants to bind (like my case). Unless that is an uncommon way to use Kodein. Do you have contribution guidelines? I can add that in

We do not support generic class type binding, even tho Kaverit has a function that could help
public expect fun <T: Any> erased(cls: KClass<T>): TypeToken<T>.

Maybe we could add something like this to the DIBuilder file

public inline fun <reified T : Any> DI.Builder.bind(
    cls: KClass<T>,
    tag: Any? = null,
    overrides: Boolean? = null,
): DI.Builder.TypeBinder<T> = Bind(erased(cls), tag, overrides)

We do not have other contribution guidelines than make a PR and we iterate on it ;)

I also made a KSP integration for Kodein (been using it at our company for 2 years) that generates Kodein modules based on annotations:

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class Component

@Component
@Suppress("unused")
class DownstreamClient (
    client: HttpClient,
    appConfig: AppConfig,
    private val singletonJobMgr: SingletonJobMgr,
    clientTimeoutMapping: ClientSocketTimeoutMapping,
    private val tokenCache: TokenCache,
    metrics: Metrics,
 )

outputs:

public class ComponentProviderImpl : ComponentProvider {
  public override fun bindAll(builder: Kodein.Builder): Unit {
        builder.bind<DownstreamClient>() with builder.singleton {
              DownstreamClient(
                  client=instance(),
                  appConfig=instance(),
                  singletonJobMgr=instance(),
                  clientTimeoutMapping=instance(),
                  tokenCache=instance(),
                  metrics=instance(),
              )
            }
  }
}

We use this so we don't need to update our bindings when we make changes to dependencies or creating new dependencies (among other things, like creating Spring-like proxy classes for cacheable functions, etc).

Do you think it could work in the scope of Kodein?

In fact, we've created Kodein to move away from annotation nightmare of Dagger / Hilt, so we are very not sur that we want to go back on that path, and going full circle. To avoid the update of bindings we generally use the new and bindSingletonOf, like:

bindSingletonOf(::MyController)
bindSingleton { new(::MyController) }

We also try to work on some KSP tools to help with simpler bindings. But its far from ready.

Did the erased(cls: KClass<T>) suggestion above helped you ?

Yeah it is working, thanks

I keep this open to keep track of it.I will add it to the core.