Bindings: Declaring dependencies

Example: initialization of a DI container
val di = DI {
	/* Bindings */
}

Bindings are declared inside a DI initialization block.

A binding always starts with bind<TYPE> { }.

There are different ways to declare bindings:

Tagged bindings

All bindings can be tagged to allow you to bind different instances of the same type.

Example: different Dice bindings
val di = DI {
    bind<Dice> { ... } (1)
    bind<Dice>(tag = "DnD10") { ... } (2)
    bind<Dice>(tag = "DnD20") { ... } (2)
}
1 Default binding (with no tag)
2 Bindings with tags ("DnD10" and "DnD20")
The tag is of type Any, it does not have to be a String.
Whether at define, at injection or at retrieval, tag should always be passed as a named argument.
Tag objects must support equality & hashcode comparison. It is therefore recommended to either use primitives (Strings, Ints, etc.) or data classes.

Provider binding

This binds a type to a provider function, which is a function that takes no arguments and returns an object of the bound type (eg. () → T).
The provided function will be called each time you need an instance of the bound type.

Example: creates a new 6 sided Dice entry each time you need one
val di = DI {
    bind<Dice> { provider { RandomDice(6) } }
}
Ending with the same result, you can also use the simple function bindProvider<Dice> { RandomDice(6) }.

Singleton binding

This binds a type to an instance of this type that will lazily be created at first use via a singleton function, which is a function that takes no arguments and returns an object of the bound type (eg. () → T).
Therefore, the provided function will be called only once: the first time an instance is needed.

Example: creates a DataSource singleton that will be initialized on first access
val di = DI {
    bind<DataSource> { singleton { SqliteDS.open("path/to/file") } }
}
Ending with the same result, you can also use the simple function bindSingleton<DataSource> { SqliteDS.open("path/to/file") }.

Non-synced singleton

By definition, there can be only one instance of a singleton, which means only one instance can be constructed. To achieve this certainty, Kodein-DI synchronizes construction. This means that, when a singleton instance is requested and not available, Kodein-DI uses a synchronization mutex to ensure that other request to the same type will wait for this instance to be constructed.

While this behaviour is the only way to ensure the singleton’s correctness, it is also costly (due to the mutex) and degrades startup performance.

If you need to improve startup performance, if you know what you are doing, you can disable this synchronization.

Example: creates a DataSource non synced singleton
val di = DI {
    bind<DataSource> { singleton(sync = false) { SqliteDS.open("path/to/file") } }
}

Using sync = false means that:

  • There will be no construction synchronicity.

  • There may be multiple instances constructed.

  • Instance will be reused as much as possible.

Eager singleton

This is the same as a regular singleton, except that the provided function will be called as soon as the DI instance is created and all bindings are defined.

Example: creates a DataSource singleton that will be initialized as soon as the binding block ends
val di = DI {
    // The SQLite connection will be opened as soon as the di instance is ready
    bind<DataSource> { eagerSingleton { SqliteDS.open("path/to/file") } }
}
Ending with the same result, you can also use the simple function bindEagerSingleton<DataSource> { SqliteDS.open("path/to/file") }.

Factory binding

This binds a type to a factory function, which is a function that takes an argument of a defined type and that returns an object of the bound type (eg. (A) → T).
The provided function will be called each time you need an instance of the bound type.

Example: creates a new Dice each time you need one, according to an Int representing the number of sides
val di = DI {
    bind<Dice> { factory { sides: Int -> RandomDice(sides) } }
}
Ending with the same result, you can also use the simple function bindFactory<Int, Dice> { sides: Int → RandomDice(sides) }.

Multi-arguments factories

You can create multi-args factories by using data classes.

Example: creates a new Dice each time you need one, according to multiple parameters
data class DiceParams(val startNumber: Int, val sides: Int)

val di = DI {
    bind<Dice> { factory { params: DiceParams -> RandomDice(params) } }
}

Multiton binding

A multiton can be thought of a "singleton factory": it guarantees to always return the same object given the same argument. In other words, for a given argument, the first time a multiton is called with this argument, it will call the function to create an instance; and will always yield that same instance when called with the same argument.

Example: creates one random generator for each value
val di = DI {
    bind<RandomGenerator> { multiton { max: Int -> SecureRandomGenerator(max) } }
}

Just like a factory, a multiton can take multiple (up to 5) arguments.

Ending with the same result, you can also use the simple function bindMultiton<Int, RandomGenerator> { max: Int → SecureRandomGenerator(max) }.

non-synced multiton

Just like a singleton, a multiton synchronization can be disabled:

Example: non-synced multiton
val di = DI {
    bind<RandomGenerator> { multiton(sync = false)  { max: Int -> SecureRandomGenerator(max) } }
}
Ending with the same result, you can also use the simple function bindMultiton<Int, RandomGenerator>(sync = false) { max: Int → SecureRandomGenerator(max) }.

Referenced singleton or multiton binding

A referenced singleton is an object that is guaranteed to be single as long as a reference object can return it. A referenced multiton is an object that is guaranteed to be single for the same argument as long as a reference object can return it.

A referenced singleton or multiton needs a "reference maker" in addition to the classic construction function that determines the type of reference that will be used.

Kodein-DI comes with three reference makers for the JVM:

JVM: Soft & weak

These are objects that are guaranteed to be single in the JVM at a given time, but not guaranteed to be single during the application lifetime. If there are no more strong references to the instances, they may be GC’d and later, re-created.

Therefore, the provided function may or may not be called multiple times during the application lifetime.

Example: creates a Cache object that will exist only once at a given time
val di = DI {
    bind<Map> { singleton(ref = softReference) { WorldMap() } } (1)
    bind<Client> { singleton(ref = weakReference) { id -> clientFromDB(id) } }(2)
}
1 Because it’s bound by a soft reference, the JVM will GC it before any OutOfMemoryException can occur.
2 Because it’s bound by a weak reference, the JVM will GC it is no more referenced.

Weak singletons use JVM’s Weak while soft singletons use JVM’s Soft.

JVM: Thread local

This is the same as the standard singleton binding, except that each thread gets a different instance. Therefore, the provided function will be called once per thread that needs the instance, the first time it is requested.

Example: creates a Cache object that will exist once per thread
val di = DI {
    bind<Cache> { singleton(ref = threadLocal) { LRUCache(16 * 1024) } }
}
Semantically, thread local singletons should use [scoped-singletons], the reason it uses a referenced singleton is because Java’s ThreadLocal acts like a reference.
Thread locals are not available in JavaScript.

Instance binding

This binds a type to an instance that already exist.

Example: a DataSource binding to an already existing instance.
val di = DI {
    bind<DataSource> { instance(SqliteDataSource.open("path/to/file")) } (1)
}
1 Instance is used with parenthesis: it is not given a function, but an instance.
Ending with the same result, you can also use the simple function bindInstance<DataSource> { SqliteDataSource.open("path/to/file") }.

Constant binding

It is often useful to bind "configuration" constants.

Constants are always tagged.
Example: two constants
val di = DI {
    bindConstant(tag = "maxThread") { 8 } (1)
    bindConstant(tag = "serverURL") { "https://my.server.url" } (1)
}
1 Note the absence of curly braces: it is not given a function, but an instance.
You should only use constant bindings for very simple types without inheritance or interface (e.g. primitive types and data classes).

Direct binding

Sometimes, it may seem overkill to specify the type to bind if you are binding the same type as you are creating.

For this use case, you can transform any bind<TYPE>() with …​ to bind { …​ }.

Example: direct bindings
val di = DI {
    bind { singleton { RandomDice(6) } }
    bind("DnD20") { provider { RandomDice(20) } }
    bind { instance(SqliteDataSource.open("path/to/file")) }
}
This should be used with care as binding a concrete class and, therefore, having concrete dependencies is an anti-pattern that later prevents modularisation and mocking / testing.
When binding a generic type, the bound type will be the specialized type,
e.g. bind { singleton { listOf(1, 2, 3, 4) } } registers the binding to List<Int>.

If you are binding straight types you can use the following extension functions:

Example: simple bindings
val di = DI {
    bindFactory { size: Int -> RandomDice(size) }
    bindProvider { RandomDice(20) }
    bindSingleton { RandomDice(6) }
    bindMultiton { name: String ->  Person(name) }
    bindConstant("answer") { 42 }
    bindInstance(SqliteDataSource.open("path/to/file"))
}

Subtypes bindings

Kodein-DI allows you register a "subtype bindings factory". These are big words for a simple concept that’s best explained with an example:

Example: direct bindings
val di = DI {
    bind<Controller>().subtypes() with { type ->
        when (type.jvmType) { (1)
            MySpecialController::class.java -> singleton { MySpecialController() }
            else -> provider { myControllerSystem.getController(type.jvmType) }
        }
    }
}
1 As type is a TypeToken<*>, you can use .jvmType to get the JVM type (e.g. Class or ParameterizedType).

In essence, bind<Whatever>().subtypes() with { type → binding } allows you to register, in Kodein-DI, a binding factory that will be called for subtypes of the provided type.

Delegate binding

In some cases you could need to bind a super type to an already bound type. For that, Kodein-DI provides a delegation mechanism, to delegate a given binding to another.

Example: delegate binding
interface ICache
class Cache : ICache

val di = DI {
    bind { singleton { Cache() } } (1)
    delegate<ICache>().to<Cache>() (2)
}
1 Binds a concrete type
2 At retrieval ICache will be exactly the same instance as Cache, bound in (1).

Transitive dependencies

With those lazily instantiated dependencies, a dependency (very) often needs another dependency. Such classes can have their dependencies passed to their constructor. Thanks to Kotlin’s killer type inference engine, Kodein-DI makes retrieval of transitive dependencies really easy.

Example: a class that needs transitive dependencies
class Dice(private val random: Random, private val sides: Int) {
/*...*/
}

It is really easy to bind this RandomDice with its transitive dependencies, by simply using instance() or instance(tag).

Example: bindings of a Dice and of its transitive dependencies
val di = DI {
    bind<Dice> { singleton { Dice(instance(), instance(tag = "max")) } } (1)

    bind<Random> {provider { SecureRandom() } } (2)
    bindConstant(tag="max"){ 5 } (2)
}
1 Binding of Dice. It gets its transitive dependencies by using instance() and instance(tag).
2 Bindings of Dice transitive dependencies.
The order in which the bindings are declared has no importance whatsoever.

If you use straight constructor parameters with no tags you can use the new(::Ref) function with the reference of the object constructor that you need to be created by Kodein-DI.

Example: bindings of a Dice and of its transitive dependencies
val di = DI {
    bind<Dice> { singleton { new(::Dice) } } (1)
}
1 Binding of Dice. It gets its transitive dependencies.

Or even more concise:

val di = DI {
    bindSingletonOf(::Dice) (1)
}
1 Binding of Dice. It gets its transitive dependencies.

Those tricks are limited to 0 to 10 constructor parameters.

The binding functions are in the same environment as the newInstance function described in the dependency injection section. You can read it to learn more about the instance, provider and factory functions available to the function.

Transitive factory dependencies

Maybe you need a dependency to use one of its functions to create the bound type.

Example: using a DataSource to create a Connection.
val di = DI {
    bind<DataSource> { singleton { MySQLDataSource() } }
    bind<Connection> { provider { instance<DataSource>().openConnection() } } (1)
}
1 Using a DataSource as a transitive factory dependency.

Being responsible for its own retrieval

If the bound class is DIAware, you can pass the di object to the class so it can itself use the DI container to retrieve its own dependencies.

Example: bindings of Manager that is responsible for retrieving its own dependencies
val di = DI {
    bind<Manager> { singleton { ManagerImpl(di) } } (1)
}
1 ManagerImpl is given a DI instance.