Dependency injection & retrieval
val di = DI {
bind<Dice> { factory { sides: Int -> RandomDice(sides) } }
bind<DataSource> { singleton { SqliteDS.open("path/to/file") } }
bind<Random> { provider { SecureRandom() } }
bind<FileAccess>() { factory { path: String, mode: Int -> FileAccess.open(path, mode) } }
bindConstant("answer") { "fourty-two" }
}
Retrieval rules
-
A dependency bound with a
provider
, aninstance
, asingleton
, aneagerSingleton
, or aconstant
can be retrieved:-
as a provider method:
() → T
-
as an instance:
T
-
-
A dependency bound with a
factory
or amultiton
can only be retrieved as a factory method:(A) → T
.-
as a factory method:
(A) → T
-
as a provider method:
() → T
if the argumentA
is provided at retrieval. -
as an instance:
T
if the argumentA
is provided at retrieval.
-
Injection & Retrieval
When dependencies are injected, the class is provided its dependencies at construction.
When dependencies are retrieved, the class is responsible for getting its own dependencies.
Using dependency injection is a bit more cumbersome, but your classes are "pure": they are unaware of the dependency container. Using dependency retrieval is easier (and allows more tooling), but it does binds your classes to the Kodein-DI API.
Finally, in retrieval, everything is lazy by default, while there can be no lazy-loading using injection.
If you are developing a library, then you probably should use dependency injection, to avoid forcing the users of your library to use Kodein-DI as well. If you are developing an application, then you should consider using dependency retrieval, as it is easier to use and provides more tooling. |
Base methods
Whether you are using dependency injection or retrieval, the same 3 methods will be available with the same name and parameters (but not return type).
These methods are:
-
instance()
if you need an instance:T
. -
provider()
if you need a provider:() → T
. -
factory()
if you need an instance:(A) → T
.
All three methods can take a tag
argument.
The Example: Using the named tag argument.
|
Injection
To use dependency injection,
-
Declare your dependencies in the constructor of your classes.
-
Use Kodein-DI's
newInstance
method to create an object of such class.
Simple case
class MainController(val ds: DataSource, val rnd: Random) { /*...*/ }
val controller by di.newInstance { MainController(instance(), instance(tag = "whatever")) } (1)
1 | Note the use of the instance function that will inject the correct dependency. |
When injecting a type that was not bound, a DI.NotFoundException will be thrown.
|
If you are not sure (or simply do not know) if the type has been bound, you can use *OrNull
methods.
Multi-arguments factories
When injecting a value that was bound with a multi-argument factory, the arguments must be wrapped inside a data class:
data class ControllerParams(val path: String, val timeout: Int)
val controller by di.newInstance { FileController(instance(args = ControllerParams("path/to/file", 0))) }
Currying factories
You can retrieve a provider or an instance from a factory bound type by using the arg
parameter (this is called currying).
class RollController(val dice: Dice) { /*...*/ }
val controller by di.newInstance { RollController(instance(arg = 6)) }
Note that if you want to bind a factory with multiple argument, you need to use a data class to pass multiple arguments:
data class Params(val arg1: Int, val arg2: Int)
val controller by di.newInstance { RollController(instance(arg = Params(60, 6))) }
The arg argument should always be named.
|
Defining context
When retrieving, you sometimes need to manually define a context (for example, when retrieving a scoped singleton).
For this, you can use the on
method:
val controller by di.on(context = myContext).newInstance { OtherController(instance(arg = 6), instance()) }
The context argument should always be named.
|
Sometimes, the context is not available directly at construction. When that happens, you can define a lazy context that will be accessed only when needed.
val controller by di.on { requireActivity() } .newInstance { OtherController(instance(arg = 6), instance()) }
Retrieval: the DI container
everything is lazy by default!
In the next few sections, we will be describing dependency retrieval. As you might have guessed by the title of this section, everything, in dependency retrieval, is lazy by default.
This allows:
-
Dependencies to be retrieved only when they are actually needed.
-
"Out of context" classes such as Android Activities to access their dependencies once their contexts have been initialized.
If you want "direct" retrieval, well, there’s a section named direct retrieval, how about that!
Kodein-DI methods
You can retrieve a bound type via a DI instance.
val diceFactory: (Int) -> Dice by di.factory()
val dataSource: DataSource by di.instance()
val randomProvider: () -> Random by di.provider()
val answerConstant: String by di.instance(tag = "answer")
Note the use of the by
.
Kodein-DI uses delegated properties to enable:
-
Lazy loading
-
Accessing the receiver
When using a provider function (() → T ), whether this function will give each time a new instance or the same depends on the binding.
|
When asking for a type that was not bound, a DI.NotFoundException will be thrown.
|
If you are not sure (or simply do not know) if the type has been bound, you can use *OrNull
methods.
val diceFactory: ((Int) -> Dice)? by di.factoryOrNull()
val dataSource: DataSource? by di.instanceOrNull()
val randomProvider: (() -> Random)? by di.providerOrNull()
val answerConstant: String? by di.instanceOrNull(tag = "answer")
Constants
If you bound constants, you can easily retrieve them with the constant method if the name of the property matches the tag:
val answer: String by di.constant()
Named bindings
If you used tagged bindings, if the tag is a String
and the property name matches the tag, instead of passing it as argument, you can use named
:
val answer: String by di.named.instance()
Multi-arguments factories
When retrieving a value that was bound with a multi-argument factory, the arguments must be wrapped inside a data class:
data class FileParams(val path: String, val maxSize: Int)
val fileAccess: FileAccess by di.instance(args = FileParams("/path/to/file", 0))
Factory retrieval
Instead of retrieving a value, you can retrieve a factory, that can call as much as you need.
val f1: (Int) -> Int by di.factory() (1)
1 | retrieving a factory that takes 1 argument (Int) and return an Int |
Currying factories
You can retrieve a provider or an instance from a factory bound type by using the arg
parameter (this is called currying).
val sixSideDiceProvider: () -> Dice by di.provider(arg = 6)
val twentySideDice: Dice by di.instance(arg = 20)
Note that if you bound a factory with multiple arguments, you need to use a data class to pass multiple arguments:
data class DiceParams(val startNumber: Int, val sides: Int)
val sixtyToSixtySixDice: Dice by di.instance(arg = DiceParams(60, 6)) (1)
1 | Bonus points if you can say the variable name 5 times in less than 5 seconds ;) |
The arg argument should always be named.
|
Defining context
Whether you are using a scoped singleton/multiton or using a context in the target binding, you may need to specify a context.
val session: Session by di.on(context = request).instance()
If you retrieve multiple dependencies all using the same context, you can create a new DI
object with the context set:
val reqDI = di.on(context = request)
val session: Session by reqDI.instance()
The context argument should always be named.
|
Using a global context does not forces you to use only bindings that are declared with this type of context.
Because the default context is Any? , all non-contexted bindings will still be available with a global context set.
|
Using a Trigger
There is a mechanism that allows you to decide when dependencies are actually retrieved if you want them to be retrieved at a particular time and not at first access. This mechanism is called a Trigger.
val trigger = DITrigger()
val dice: Dice by di.on(trigger = trigger).instance()
/*...*/
trigger.trigger() (1)
1 | Retrieval happens now. |
You can, of course, assign multiple properties to the same trigger. You can also create a DI object that has a given trigger by default:
val trigger = DITrigger()
val injectDI = di.on(trigger = trigger)
val dice: Dice by injectDI.instance()
/*...*/
trigger.trigger()
The trigger argument should always be named.
|
A trigger allows you to "force" retrieval.
However, retrieval can still happen before inject() is called if the variable is accessed.
|
Lazy access
Kodein-DI proposes a LazyDI
object that allows you to lazily access the DI object only when needed.
This is useful if:
-
You need to defined a lazily retrieved dependency before having access to a DI container.
-
You don’t know if you’ll ever need to access a DI object.
For this, you can use a LazyDI
:
val di = LazyDI { /* access to a di instance */ }
val ds: DataSource by di.instance()
/*...*/
dice.roll() (1)
1 | Only then will the DI instance will itself be retrieved. |
Note that you can also lazily create a DI
object so that the bindings definition function will only be called when the first retrieved property is needed:
val di by DI.lazy {
bind<Env> { instance(Env.getInstance()) }
}
val env: Env by di.instance()
/*...*/
env.doSomething() (1)
1 | Only then will the DI instance will itself be created, and the bindings definition function ran. |
Late init
Kodein-DI proposes a LateInitDI
that allows you to define a DI object after some lazy retrieval:
val di = LateInitDI()
val env: Env by di.instance()
/*...*/
di.baseDI = /* access to a di instance */ (1)
/*...*/
env.doSomething() (2)
1 | Setting the real DI object. |
2 | If this was run before setting di.baseDI , an UninitializedPropertyAccessException would be thrown. |
All matches
Kodein-DI allows you to retrieve all instances that matches a given type:
val instances: List<Foo> by di.allInstances() (1)
1 | Will return all instances that are for bindings of sub-classes of Foo |
Of course, allProviders and allFactories are also provided ;)
|
Retrieval: being DIAware
Simple retrieval
You can have classes that implement the interface DIAware
.
Doing so has the benefit of getting a simpler syntax for retrieval.
class MyManager(override val di: DI) : DIAware {
private val diceFactory: ((Int) -> Dice)? by factoryOrNull()
private val dataSource: DataSource? by instanceOrNull()
private val randomProvider: (() -> Random)? by providerOrNull()
private val answerConstant: String? by instanceOrNull(tag = "answer")
private val sixSideDiceProvider: () -> Dice by di.provider(arg = 6)
private val twentySideDice: Dice by di.instance(arg = 20)
}
All methods that are available to the DI container are available to a DIAware
class.
Class global context
In a DIAware
class, to define a context that’s valid for the entire class, you can simply override the diContext
property:
class MyManager(override val di: DI) : DIAware {
override val diContext = kcontext(whatever) (1)
/*...*/
}
1 | Note the use of the diContext function that creates a DIContext with the given value. |
Using a global context does not forces you to use only bindings that are declared with this type of context.
Because the default context is Any? , all non-contexted bindings will still be available with a global context set.
|
Sometimes, the context is not available directly at construction. When that happens, you can define a lazy context that will be accessed only when needed.
class MyManager(override val di: DI) : DIAware {
override val diContext = kcontext { requireActivity }
/*...*/
}
Class global trigger
If you want to have all dependency properties retrieved at once, you can use a class global trigger.
Simply override the diTrigger
property:
class MyManager(override val di: DI) : DIAware {
override val diTrigger = DITrigger()
val ds: DataSource by instance()
/*...*/
fun onReady() {
diTrigger.trigger() (1)
}
}
1 | Retrieval of all dependencies happens now. |
Lazy access
Some classes (such as Android Activities) do not have access to a DI
instance at the time of construction, but only later when they have been properly connected to their environment (Android context).
Because DI is lazy by default, this does not cause any issue: simply have the di
property be lazy by itself:
di
class MyActivity : Activity(), DIAware {
override val di by lazy { (applicationContext as MyApplication).di }
val ds: DataSource by instance() (1)
}
1 | Because ds is lazily retrieved, access to the di property will only happen at first retrieval. |
There is an official module to ease the use of DI in Android, you can read more about it on the dedicated document. |
Lateinit
Because everything is lazy and, in a DIAware class, the DI object is not accessed until needed, you can easily declare the di
field as lateinit.
di
class MyActivity : Activity(), DIAware {
override val lateinit di: DI
val ds: DataSource by instance() (1)
override fun onCreate(savedInstanceState: Bundle?) {
di = (applicationContext as MyApplication).di
}
}
1 | Because ds is lazily retrieved, access to the di property will only happen at first retrieval. |
Retrieval: Direct
If you don’t want to use delegated properties, Kodein-DI has you covered.
Most of the features available to DI
are available to DirectDI
.
DirectDI
allows you to directly get a new instance or dependency.
However, because it is direct, DirectDI
does NOT feature:
-
Laziness: the instance/provider/factory is fetched at call time.
-
Receiver awareness: receiver is defined by the Kotlin’s delegated properties mechanism.
val directDI = di.direct
val ds: Datasource = directDI.instance()
val controller = directDI.newInstance { MainController(instance(), instance(tag = "whatever")) }
If you only plan to use direct access, you can define your main di object to be a Example: using a DirectDI
<1>: Note the |
Being DirectDIAware
Much like DI
offers DIAware
, DirectDI
offers DirectDIAware
class MyManager(override val directDI: DirectDI) : DirectDIAware {
private val diceFactory: ((Int) -> Dice)? = factoryOrNull()
private val dataSource: DataSource? = instanceOrNull()
private val randomProvider: (() -> Random)? = providerOrNull()
private val answerConstant: String? = instanceOrNull(tag = "answer")
private val sixSideDiceProvider: () -> Dice = di.provider(arg = 6)
private val twentySideDice: Dice = di.instance(arg = 20)
}
In Java
While Kodein-DI does not allow you to declare modules or dependencies in Java, it does allow you to retrieve dependencies via DirectDI
.
Simply give the DirectDI instance to your Java classes, use Kodein-DI in Java with the erased
static function:
import static org.kodein.type.erased;
public class JavaClass {
private final Function1<Integer, Dice> diceFactory;
private final Datasource dataSource;
private final Function0<Random> randomProvider;
private final String answerConstant;
public JavaClass(DirectDI di) {
diceFactory = di.Factory(erased(Integer.class), erased(Dice.class), null);
dataSource = di.Instance(erased(Datasource.class), null);
randomProvider = di.Provider(erased(Random.class), null);
answerConstant = di.Instance(erased(String.class), "answer");
}}
Remember that Java is subject to type erasure.
Therefore, if you registered a generic Class binding such as Example: using TypeReference in Java
|
Error messages
By default, Kodein-DI error messages contains the classes simple names (e.g. View
), which makes it easily readable.
If you want the error to contain classes full names (e.g. com.company.app.UserController.View
), you can set fullDescriptionOnError
:
val di = DI {
fullDescriptionOnError = true
}
If you are using multiple DI instances, you can set the default value fullDescriptionOnError
for all subsequently created DI instances:
DI.defaultFullDescriptionOnError = true
DI.defaultFullDescriptionOnError must be set before creating a DI instance.
|