Logo

Redin is a simple dependency injection framework, instead of having a bunch of features, we provide a limited set of features that must be enough for the majority of projects, mainly the smaller ones.

Basic injection

Redin provides a Kotlin DSL for declaration of dependencies:

val injector = Redin {
    bind<AccountService>() toImplementation(AccountServiceImpl::class.java)
}

val serviceProvider = injector.provide<AccountService>()

Note that, the provide function returns a function that, when invoked, produces the underlying requested dependency. This means that, every time you do serviceProvider() it creates a new instance of this service.

If that is not what you want, you could use scopes to define the scope of the dependency. Once an instance of a dependency is produced in a given scope, the instance keeps tied to that scope. So, calling the serviceProvider always yields the same dependency instance. The most commonly used scope is SINGLETON:

val injector = Redin {
    bind<AccountService>() inScope SINGLETON toImplementation(AccountServiceImpl::class.java)
}

val service = injector.provide<AccountService>(scope = SINGLETON)

Also, you could use qualifiers to identify between different instances of the same type of dependency, for example, for services urls:

val injector = Redin {
    bind<String>() qualifiedWith Name("accountService") inScope SINGLETON toValue "https://service/"
    bind<String>() qualifiedWith Name("transactionService") inScope SINGLETON toValue "https://service2/"
}

val service = injector.provide<String>(
    scope = SINGLETON,
    qualifiers = listOf(nameContainer("transactionService"))
)

Injecting in classes

To use class injection, you need to annotate the constructor to be used to inject the dependencies with @Inject, see an example below:

data class Account(val id: Long)

interface AccountService {
    fun deposit(account: Account, amount: Long)
}


class AccountServiceImpl : AccountService {
    override fun deposit(account: Account, amount: Long) {
        // Deposit logic
    }
}

class BankService @Inject constructor(val accountService: AccountService)

fun inject() {
    val injector = Redin {
        bind<AccountService>() inScope SINGLETON toImplementation(AccountServiceImpl::class.java)
        bind<BankService>() inScope SINGLETON toImplementation(BankService::class.java)
    }

    val accountService = injector.provide<AccountService>(scope = SINGLETON)
    val bankService = injector.provide<BankService>(scope = SINGLETON)
}

Adding more bindings to existing injector

To add more bindings, just call bind method, it will create a new BindContext:

fun inject() {
    val injector = Redin {
        bind<AccountService>() inScope SINGLETON toImplementation(AccountServiceImpl::class.java)
    }

    val accountService = injector.provide<AccountService>(scope = SINGLETON)
    injector.bind {
        bind<BankService>() inScope SINGLETON toImplementation(BankService::class.java)
    }

    val bankService = injector.provide<BankService>(scope = SINGLETON)
}

Child injectors

Child injectors could be used to inherit dependencies from a parent injector, while providing a whole new scope of dependencies. One example of usage of child injector is in plugin system, to provide a dedicated Logger for every plugin.

The example below shows an example of a “plugin system” taking advantage of child injectors:

data class Project(val name: String)
abstract class Plugin {
    abstract fun init()
}
class PluginA @Inject constructor(val logger: Logger, val project: Project, val injector: Injector): Plugin() {
    override fun init() {
        logger.info("Plugin A initialized for project ‘${this.project.name}’!")
    }
}
class PluginB @Inject constructor(val logger: Logger, val project: Project, val injector: Injector): Plugin() {
    override fun init() {
        logger.info("Plugin B initialized for project ‘${this.project.name}’!")
    }
}

val injector = Redin {
    bind<Project>() inScope SINGLETON toValue Project("Test")
}

val pluginClasses = listOf(PluginA::class.java, PluginB::class.java)
val pluginInstances = mutableListOf<Plugin>()

for (pluginClass in pluginClasses) {
    val pluginInjector = injector.child {
        bind<Logger>().inSingletonScope().toValue(Logger.getLogger(pluginClass.name))
    }

    pluginInstances.add(pluginInjector[pluginClass])
}


pluginInstances.forEach(Plugin::init)

Retrieving all bindings of a given type

In scenarios like this one, you may want to retrieve all Plugin instances. Redin tries to not introduce any unexpected behavior, then a regular binding won't work, since binding to the same type overwrites the previous binding. To access all instances of a given type, you need to register a binding for a provider which carries all those instances.

You can also use qualifiers to automatically provide these instances using an identifier as well.

The example below shows how to provide all instances of a given type through a List, as well as using qualifiers to provide dedicated bindings:

data class Project(val name: String)
abstract class Plugin {
    abstract val id: String
    abstract fun init()
}
class PluginA @Inject constructor(val logger: Logger, val project: Project, val injector: Injector): Plugin() {
    override val id: String = "com.example.PluginA"

    override fun init() {
        logger.info("Plugin A initialized for project ‘${this.project.name}’!")
    }
}
class PluginB @Inject constructor(val logger: Logger, val project: Project, val injector: Injector): Plugin() {
    override val id: String = "com.example.PluginB"

    override fun init() {
        logger.info("Plugin B initialized for project ‘${this.project.name}’!")
    }
}
class PluginC @Inject constructor(val logger: Logger,
                                  val project: Project,
                                  @Named("com.example.PluginB") val pluginB: Plugin,
                                  val injector: Injector): Plugin() {
    override val id: String = "com.example.PluginC"

    override fun init() {
        logger.info("Plugin C initialized for project ‘${this.project.name}’!")
    }
}

val injector = Redin {
    bind<Project>() inScope SINGLETON toValue Project("Test")
}

val pluginClasses = listOf(PluginA::class.java, PluginB::class.java, PluginC::class.java)
val pluginInstances = mutableListOf<Plugin>()

injector.bind {
    bindReified<List<Plugin>>().inSingletonScope().toProvider { pluginInstances }
}

for (pluginClass in pluginClasses) {
    val pluginInjector = injector.child {
        bind<Logger>().inSingletonScope().toValue(Logger.getLogger(pluginClass.name))
    }

    val pluginInstance = pluginInjector[pluginClass]
    pluginInstances.add(pluginInjector[pluginClass])

    injector.bind {
        bind<Plugin>().inSingletonScope().qualifiedWith(Name(pluginInstance.id)).toValue(pluginInstance)
    }
}


pluginInstances.forEach(Plugin::init)

class PluginSystem @Inject constructor(@Singleton val plugins: List<Plugin>)

injector.bind {
    bind<PluginSystem>().inSingletonScope().toImplementation<PluginSystem>()
}

val system = injector.provide<PluginSystem>(scope = SINGLETON)()

The bindReified function is used when you want to bind keeping the generic type information, rather than erasing the type information. You could also use bind(TypeInfo.builderOf(List::class.java).of(Plugin::class.java).buildGeneric())

Late binding

Redin also supports late binding mechanism, it does by injecting the dependencies when they are needed, instead of injecting them right in the construction time:

class MyPlugin @Inject constructor(@Late val globalLogger: LateInit.Ref<Logger>) {

    fun log(message: String) {
        this.globalLogger.value.info("MyPlugin: ‘$message’")
    }

}

fun lateInject() {
    val injector = Redin {
        bind<MyPlugin>().inSingletonScope().toImplementation<MyPlugin>()
    }

    val myPlugin = injector.provide<MyPlugin>(scope = SINGLETON)()

    //myPlugin.log("Hello") // will fail

    injector.bind {
        bind<Logger>().inSingletonScope().toValue(Logger.getGlobal())
    }

    myPlugin.log("Hello") // works!

}

Lazy binding

Works in the same way as late binding:

class MyPlugin @Inject constructor(@LazyDep val globalLogger: Lazy<Logger>) {

    fun log(message: String) {
        this.globalLogger.value.info("MyPlugin: ‘$message’")
    }

}


fun lazyInject() {
    val injector = Redin {
        bind<MyPlugin>().inSingletonScope().toImplementation<MyPlugin>()
    }

    val myPlugin = injector.provide<MyPlugin>(scope = SINGLETON)()

    myPlugin.log("Hello") // will fail

    injector.bind {
        bind<Logger>().inSingletonScope().toValue(Logger.getGlobal())
    }

    myPlugin.log("Hello") // works!

}

The main difference is that late injection is handled by Injector, when new bindings are added, the engine tries to resolve late injection points, while lazy injection is handled by a Lazy implementation that search by available binding at the first use. In other words, while late injection is resolved as soon as a candidate is provided, lazy injection is only resolved when the value is used.

In practice, you could choose between late or lazy, but it only applies when using LateInit or Kotlin Lazy types: Lazy bindings are also candidates for dynamic generation, so the code below works fine:

interface Logger {
    fun info(message: String)
}

class MyPlugin @Inject constructor(@LazyDep val globalLogger: Logger) {

    fun log(message: String) {
        this.globalLogger.info("MyPlugin: ‘$message’")
    }

}

fun dynamicLazyInject() {
    val injector = Redin {
        bind<MyPlugin>().inSingletonScope().toImplementation<MyPlugin>()
    }

    val myPlugin = injector.provide<MyPlugin>(scope = SINGLETON)()

    //myPlugin.log("Hello") // will fail

    injector.bind {
        bind<Logger>().inSingletonScope().toValue(object : Logger {
            override fun info(message: String) {
                java.util.logging.Logger.getGlobal().info(message)
            }
        })
    }

    myPlugin.log("Hello") // works!

}

Redin uses KoresProxy to generate dynamic implementation. However, while this may sound that invocations are dynamic, they are not: Redin generates proxies that uses static invocation, so there is no performance overhead in using them.

Hot Swap

Redin allows for switching existing injections into new ones, this is possible through HotSwappable.

interface Logger {
    fun info(message: String)
}

class PrefixedLogger(val prefix: String): Logger {
    override fun info(message: String) {
        println("$prefix: $message")
    }
}

class MyPlugin @Inject constructor(@HotSwappable val globalLogger: Hot<Logger>) {

    fun log(message: String) {
        this.globalLogger.value.info("MyPlugin: ‘$message’")
    }

}


fun hotInject() {
    val injector = Redin {
        bind<Logger>().inSingletonScope().toValue(PrefixedLogger("First"))
        bind<MyPlugin>().inSingletonScope().toImplementation<MyPlugin>()
    }

    val myPlugin = injector.provide<MyPlugin>(scope = SINGLETON)()

    myPlugin.log("Hello")

    injector.bind {
        bind<Logger>().inSingletonScope().toValue(PrefixedLogger("Swapped"))
    }

    myPlugin.log("Hello")

}

Proxies can be used like in lazy injection, with zero runtime overhead (only class generation overhead, which happens only once in the entire execution):

interface Logger {
    fun info(message: String)
}

class PrefixedLogger(val prefix: String): Logger {
    override fun info(message: String) {
        println("$prefix: $message")
    }
}

class MyPlugin @Inject constructor(@HotSwappable val globalLogger: Logger) {

    fun log(message: String) {
        this.globalLogger.info("MyPlugin: ‘$message’")
    }

}


@Test
fun hotInject() {
    val injector = Redin {
        bind<Logger>().inSingletonScope().toValue(PrefixedLogger("First"))
        bind<MyPlugin>().inSingletonScope().toImplementation<MyPlugin>()
    }

    val myPlugin = injector.provide<MyPlugin>(scope = SINGLETON)()

    myPlugin.log("Hello")

    injector.bind {
        bind<Logger>().inSingletonScope().toValue(PrefixedLogger("Swapped"))
    }

    myPlugin.log("Hello")

}

Module

Redin supports module-like bind declaration:

@RedinInject
class Example(val log: LoggingService)
class MyModule {
    @Provides
    @Singleton
    fun provideLoggingService(): LoggingService = MyLoggingService()
}

fun example() {
    val injector = Redin {
        module(MyModule())
    }
    
    val example = injector.get<Example>()
}

get vs provide

The get function is used to create an instance of a class injecting dependencies as needed, while provide is used to retrieve a dependency declared in BindContext:

class MyService @Inject constructor(val logger: Logger)

class MyService2 @Inject constructor(val myService: MyService)

@Test
fun getVsProvideInject() {
    val injector = Redin {
        bind<Logger>().inSingletonScope().toValue(Logger.getGlobal())
    }
    
    // Creates a new instance of MyService injecting dependencies
    val myService = injector.get<MyService>()
    // Won't work since there is no binding for ‘MyService’.
    val myServiceProvided = injector.provide<MyService>(scope = SINGLETON)()
    // Won't work since there is no binding for ‘MyService’ to inject in ‘MyService2’.
    val myService2 = injector.get<MyService2>()

}

The right way to write the could above is:

class MyService @Inject constructor(val logger: Logger)

class MyService2 @Inject constructor(val myService: MyService)

fun getVsProvideInject() {
    val injector = Redin {
        bind<Logger>().inSingletonScope().toValue(Logger.getGlobal())
        bindToImplementation<MyService>(scope = SINGLETON)
    }

    val myService = injector.provide<MyService>(scope = SINGLETON)()
    val myServiceProvided = injector.provide<MyService>(scope = SINGLETON)()
    val myService2 = injector.get<MyService2>()

}

Lazy by default

Redin resolves dependencies lazily by default, this means that the following code works:


class MyService @Inject constructor(val logger: Logger)

fun lazyByDefaultInject() {
    val injector = Redin {
        bind<Logger>().inSingletonScope().toValue(Logger.getGlobal())
    }

    val myService = injector.provide<MyService>(scope = SINGLETON)

    injector.bind {
        bindToImplementation<MyService>(scope = SINGLETON)
    }

    myService()

}

Resolution only occurs when the provider function is invoked. However, since classes does not have this indirection, they always resolve dependencies in instantiation (unless they have @Late or @Lazy dependencies).

Circular dependency

A circular dependency scenario occurs when, for example, a dependency X depends on a dependency Y that depends on the dependency X.

This could occur directly or indirectly, for example, the code below shows an example of a direct circular dependency:

class DependencyA @Inject constructor(val dependencyB: DependencyB)
class DependencyB @Inject constructor(val dependencyA: DependencyA)

and the code below shows and example of indirect circular dependency:

class DependencyA @Inject constructor(val dependencyB: DependencyB)
class DependencyB @Inject constructor(val dependencyC: DependencyC)
class DependencyC @Inject constructor(val dependencyA: DependencyA)

Redin does not have a mechanism to detect circular dependencies, because even if it is possible to do, given that Redin allows Dependency Providers which resolves dependencies dynamically, it will not be possible to cover all scenarios without adding extra work to users of Redin. So circular dependencies ends up in a StackOverflowError.

If you really need to have a circular dependency, use lazy dependency resolution:

class DependencyA @Inject constructor(val dependencyB: DependencyB)
class DependencyB @Inject constructor(@LazyDep val dependencyA: Lazy<DependencyA>)

fun circularInject() {
    val injector = Redin {
        bind<DependencyA>().inSingletonScope().toImplementation<DependencyA>()
        bind<DependencyB>().inSingletonScope().toImplementation<DependencyB>()
    }

    val dependency = injector.provide<DependencyA>(scope = SINGLETON)()

}

However, we heavily discourage the use of circular dependencies, as it does not follow the separation of concerns concept.

If you have a common interface, it would be interesting to use the proxied version of lazy, as it introduces lazy initialization without Lazy type indirection:

interface DependencyA
interface DependencyB

class DependencyAImpl @Inject constructor(val dependencyB: DependencyB) : DependencyA
class DependencyBImpl @Inject constructor(@LazyDep val dependencyA: DependencyA) : DependencyB


fun circularInject() {
    val injector = Redin {
        bind<DependencyA>().inSingletonScope().toImplementation<DependencyAImpl>()
        bind<DependencyB>().inSingletonScope().toImplementation<DependencyBImpl>()
    }

    val dependency = injector.provide<DependencyA>(scope = SINGLETON)()

}

Scope

Scopes defines the “scope” of the dependency, the most common scope is the SINGLETON scope and NO_SCOPE.

NO_SCOPE

Means that no scope is used, in this case, every time a dependency is requested, a new instance is produced (unless the dependency was bind using toValue).

SINGLETON

Reuses already created instances everytime the dependency is requested.

User-defined scopes

Works the same way as SINGLETON, however instances are shared across the user defined scope instead of SINGLETON.

Implementing a scope

Every scope is linked to an annotation, so to implement your own scope, you first need to have an annotation:

@Retention(AnnotationRetention.RUNTIME)
annotation class MyScope

Then you create an object to implement BindScope:

val MY_SCOPE = object : BindScope {
    override fun match(scope: AnnotationContainer): Boolean =
        scope.type.`is`(MyScope::class.java)

    override fun toString(): String = "MY_SCOPE"
}

The usage is the same as for SINGLETON:

class ScopeTest

fun myScope() {
    val injector = Redin {
        bind<ScopeTest>() inScope MY_SCOPE toImplementation(ScopeTest::class.java)
    }

    val test = injector.provide<ScopeTest>(scope = MY_SCOPE)()
    val test2 = injector.provide<ScopeTest>(scope = MY_SCOPE)()

    println(test)
    println(test2)
}

Qualifiers

Qualifiers are used to identify between different dependencies of the same type. The most common qualifier is the Named qualifier:

val injector = Redin {
    bind<String>() inScope SINGLETON qualifiedWith Name("databaseUri") toValue "localhost:8090"
}

To query dependencies by qualifier, we need to use AnnotationContainer, however we heavily recommend providing wrappers around AnnotationContainer for the ease of use, just like we do for Named annotations:

val injector = Redin {
    bind<String>() inScope SINGLETON qualifiedWith Name("databaseUri") toValue "localhost:8090"
}

val uri = injector.provide<String>(scope = SINGLETON, qualifiers = listOf(nameQualifier("databaseUri")))()

Implementing your own qualifiers

Just like implementing a BindScope, you need an annotation class (it must be annotated with @Qualifier to be detected as a qualifier):

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MyQualifier(val name: String)

A wrapper around AnnotationContainer for the ease of use:

fun myQualifier(name: String) =
    AnnotationContainer<MyQualifier>(mapOf("name" to name))

Then the matcher:

data class MyBindQualifier(val name: String) : BindQualifier {
    override fun matches(annotationContainer: AnnotationContainer): Boolean =
        annotationContainer.type.`is`(MyQualifier::class.java)
                && annotationContainer["name"] == name

}

Using the qualifier

Use it just like any other qualifier:

class ToInject
class QualifierTest @Inject constructor(@MyQualifier("test") val inject: ToInject)

fun myQualifier() {
    val injector = Redin {
        bind<ToInject>() inScope SINGLETON qualifiedWith MyBindQualifier("test") toValue ToInject()
        bindToImplementation<QualifierTest>(scope = SINGLETON)
    }

    val qualifierTest = injector.provide<QualifierTest>(scope = SINGLETON)()
}

For querying using provide:

val toInject = injector.provide<ToInject>(scope = SINGLETON, qualifiers = listOf(myQualifier("test")))()

Kotlin Delegate

Redin supports dependency resolution using kotlin delegate (by) through Provide and Get classes:

class MyService @Inject constructor(val injector: Injector) {
    val logger: Logger by Provide(this.injector, scope = SINGLETON)
}

fun kotlinDelegateInject() {
    val injector = Redin {
        bind<Logger>().inSingletonScope().toValue(Logger.getGlobal())
        bindToImplementation<MyService>(scope = SINGLETON)
    }

    val myService = injector.provide<MyService>(scope = SINGLETON)()
}

Troubleshooting

A section dedicated to help with common mistakes made in Redin, as it does not works in the same way traditional DI works.