Bindings: Declaring dependencies
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.
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.
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.
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.
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.
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") } }
}
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.
val di = DI {
bind<Dice> { factory { sides: Int -> RandomDice(sides) } }
}
Ending with the same result, you can also use the simple function bindFactory<Int, DataSource> { sides: Int → RandomDice(sides) } .
|
Multi-arguments factories
You can create multi-args factories by using data classes.
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.
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:
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.
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.
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.
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. |
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 { … }
.
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
|
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:
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.
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.
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)
.
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. |
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.
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.
val di = DI {
bind<Manager> { singleton { ManagerImpl(di) } } (1)
}
1 | ManagerImpl is given a DI instance. |