Modules & inheritance
Modules
Definition
Kodein-DI allows you to export your bindings in modules. It is very useful to have separate modules defining their own bindings instead of having only one central binding definition. A module is an object that you can construct the exact same way as you construct a DI instance.
val apiModule = DI.Module(name = "API") {
bind<API> { singleton { APIImpl() } }
/* other bindings */
}
Then, in your DI binding block:
val di = DI {
import(apiModule)
/* other bindings */
}
Modules are definitions, they will re-declare their bindings in each DI instance you use. If you create a module that defines a singleton and import that module into two different DI instances, then the singleton object will exist twice: once in each DI instance. |
Name uniqueness
Each module name should only be imported once.
If a second module with the name of an already imported module is imported, then Kodein-DI will fail.
However, you cannot always ensure that every module name is unique: you may need to import modules that are defined outside of your code. Kodein-DI offers two ways to mitigate that:
-
Rename a module:
Use when you are importing a module whose name already exists.Example: imports a renamed moduleval di = DI { import(apiModule.copy(name = "otherAPI")) }
-
Add a prefix to modules imported by a module:
Use when a module imported by another module uses a names which already exists.Example: imports a module with a prefix for sub-modulesval di = DI { import(apiModule.copy(prefix = "otherAPI-")) }
Import once
You may define a module which you know depends on another module, so it would be great to import that dependency inside the module that has the dependency. However, each module can only be imported once, so if every module that depends on another module imports it, Kodein-DI will fail at the second module that imports it.
To support this, Kodein-DI offers importOnce
: it imports the module if no module with that name was previously imported.
val appModule = DI.Module {
importOnce(apiModule)
}
Extension (composition)
Kodein-DI allows you to create a new DI instance by extending an existing one.
val subDI = DI {
extend(appDI)
/* other bindings */
}
This preserves bindings, meaning that a singleton in the parent DI will continue to exist only once. Both parent and child DI objects will give the same instance. |
Overriding
By default, overriding a binding is not allowed in Kodein-DI. That is because accidentally binding twice the same (class,tag) to different instances/providers/factories can cause real headaches to debug.
However, when intended, it can be really interesting to override a binding, especially when creating a testing environment. You can override an existing binding by specifying explicitly that it is an override.
val di = DI {
bind<API> { singleton { APIImpl() } }
/* ... */
bind<API>(overrides = true) { singleton { OtherAPIImpl() } }
}
By default, modules are not allowed to override, even explicitly. You can allow a module to override some of your bindings when you import it (the same goes for extension):
val di = DI {
/* ... */
import(testEnvModule, allowOverride = true)
}
The bindings in the module still need to specify explicitly the overrides. |
Sometimes, you just want to define bindings without knowing if you are actually overriding a previous binding or defining a new. Those cases should be rare and you should know what you are doing.
val testModule = DI.Module(name = "test", allowSilentOverride = true) {
bind<EmailClient>() { singleton { MockEmailClient() } } (1)
}
1 | Maybe adding a new binding, maybe overriding an existing one, who knows? |
If you want to access an instance retrieved by the overridden binding, you can use overriddenInstance. This is useful if you want to "enhance" a binding (for example, using the decorator pattern).
val testModule = DI.Module(name = "test") {
bind<Logger>(overrides = true) { singleton { FileLoggerWrapper("path/to/file", overriddenInstance()) } } (1)
}
1 | overriddenInstance() will return the Logger instance retrieved by the overridden binding. |
Overridden access from parent
Let’s consider the following code :
val parent = DI {
bind<Foo>() { provider { Foo1() } }
bind<Bar>() { singleton { Bar(foo = instance<Foo>()) } }
}
val child = DI {
extend(parent)
bind<Foo>(overrides = true) { provider { Foo2() } }
}
val foo = child.instance<Bar>().foo
In this example, the foo
variable will be of type Foo1
.
Because the Bar
binding is a singleton
and is declared in the parent
Kodein-DI, it does not have access to bindings declared in child
.
In this example, both parent.instance<Bar>().foo
and child.instance<Bar>().foo
will yield a Foo1
object.
This is because Bar is bound to a singleton , the first access would define the container used (parent or child ).
If the singleton were initialized by child , then a subsequent access from parent would yield a Bar with a reference to a Foo2 , which is not supposed to exist in parent .
|
By default, all bindings that do not cache instances (basically all bindings but singleton and multiton ) are copied by default into the new container, and therefore have access to the bindings & overrides of this new container.
|
If you want the Bar
singleton to have access to the overridden Foo
binding, you need to copy it into the child
container.
val child = DI {
extend(parent, copy = Copy {
copy the binding<Bar>() (1)
})
bind<Foo>(overrides = true) { provider { Foo2() } }
}
Copying a binding means that it will exists once more. Therefore, a copied singleton will no longer be unique and have TWO instances, one managed by each binding (the original and the copied). |
If the binding you need to copy is bound by a context (such as a scoped singleton), you need to specify it:
val parent = DI {
bind<Session>(tag = "req") { scoped(requestScope).singleton { context.session() } }
}
val child = DI {
extend(parent, copy = Copy {
copy the binding<Session>() { scope(requestScope) and tag("req") }
})
bind<Foo>(overrides = true) { provider { Foo2() } }
}
You can use the context<>() , scope() and tag() functions to specialise your binding copies.
|
You can also copy all bindings that matches a particular definition :
val child = DI {
extend(parent, copy = Copy {
copy all binding<String>() (1)
copy all scope(requestScope) (2)
})
}
1 | Will copy all bindings for a String , with or without a context, scope, tag or argument. |
2 | Will copy all bindings that are scoped inside a RequestScope . |
Finally, you can simply copy all bindings:
val child = DI {
extend(parent, copy = Copy.All)
}
Or you can decide that none are copied (if you do want existing bindings to have access to new bindings):
val child = DI {
extend(parent, copy = Copy.None)
}