Kodein-DI and Compose (Android or Desktop)
You can use Kodein-DI as-is in your Android / Desktop project, but you can level-up your game by using the library kodein-di-framework-compose
.
Kodein-DI is compatible with both Jetpack and JetBrains Compose |
kodein-di-framework-compose relies and only few stable APIs that have some good chance to stay (like @Composable ).
This means that kodein-di-framework-compose doesn’t lock you with a specific version of Jetpack and JetBrains Compose.
You can use the one that works for you, as long as your kotlin version aligns with the version used to build the Compose version your are using.
|
Here is a table containing the version compatibility:
Kodein-DI |
Compose compiler |
Kotlin |
7.7.0 |
1.0.0-alpha1 |
1.5.30 |
7.6.0 |
1.0.0-beta08 |
1.5.21 |
7.5.1 |
1.0.0-beta07 |
1.4.32 |
7.5.0 |
1.0.0-beta06 |
1.4.31 |
Install
Kodein-DI for Compose can be used for Android or Desktop projects with the same approach (thanks to Gradle’s metadatas).
Start by adding the correct dependency to your Gradle build script:
implementation 'org.kodein.di:kodein-di-framework-compose:7.8.0'
implementation("org.kodein.di:kodein-di-framework-compose:7.8.0")
If you are NOT using Gradle 6+, you should declare the use of the Gradle Metadata experimental feature settings.gradle.kts
enableFeaturePreview("GRADLE_METADATA") |
Using On Android it will transitively add the specific module |
DI capabilities in a @Composable
tree
Kodein-DI fully integrates with Compose by providing:
-
an easy way to make your DI containers accessible from anywhere in your
@Composable
tree. -
some helper functions to directly retrieve your bindings in your
@Composable
functions.
Using the @Composable
hierarchy
Compose provides a way of exposing objects and instances to the @Composable
hierarchy without passing arguments through every @Composable
functions, it is called CompositionLocal.
This is what Kodein-DI uses under the hood to help you access your DI containers transparently.
Share a DI reference inside a @Composable
tree
You can easily use Kodein-DI to expose a DI container within a @Composable
tree, using the withDI
functions.
These functions accept either, a DI builder, a DI reference or DI modules.
@Composable
treeval di = DI {
bindProvider<Dice> { RandomDice(0, 5) }
bindSingleton<DataSource> { SqliteDS.open("path/to/file") }
}
@Composable
fun App() = withDI(di) { (1)
MyView { (2)
ContentView() (2)
BottomView() (2)
}
}
1 | attaches the container di to the current @Composable node |
2 | every underlying @Composable element can access the bindings declared in di |
@Composable
treeval diceModule = DI.Module("diceModule") {
bindProvider<Dice> { RandomDice(0, 5) }
}
val persistenceModule = DI.Module("persistenceModule") {
bindSingleton<DataSource> { SqliteDS.open("path/to/file") }
}
@Composable
fun App() = withDI(diceModule, persistenceModule) { (1)
MyView { (2)
ContentView() (2)
BottomView() (2)
}
}
1 | creates a DI container with the given modules before attaching it to the current @Composable node |
2 | every underlying @Composable element can access the bindings declared in diceModule and persistenceModule |
@Composable
tree@Composable
fun App() = withDI({ (1)
bindProvider<Dice> { RandomDice(0, 5) }
bindSingleton<DataSource> { SqliteDS.open("path/to/file") }
}) {
MyView { (2)
ContentView() (2)
BottomView() (2)
}
}
1 | DI builder that will be invoked and attached to the current @Composable node |
2 | every underlying @Composable element can access the bindings attached to the current @Composable node |
It’s important to understand that the bindings can’t be accessed with the CompositionLocal mechanism from the sibling or upper nodes.
The DI reference si only available inside the content lambda and for underlying @Composable element of the withDI functions.
|
Access a DI container from @Composable
functions
This assumes you have already gone through the share DI within a @Composable
tree section and that you have a DI container attached to your current @Composable
hierarchy.
Kodein-DI uses the Compose notion of CompositionLocal
to share your DI references via the withDI
and subDI
functions.
Therefore, in any underlying @Composable
function you can access the DI attached to the context with the property LocalDI
.
@Composable
fun ContentView() {
val di = LocalDI.current (1)
val dice: Dice by di.instance() (2)
}
1 | Get the DI container attache to a parent node |
2 | Standard Kodein-DI binding retrieval |
Using LocalDI in a tree where there is no DI container will throw a runtime exception: IllegalStateException: Missing DI container! .
|
Extend an existing DI container
In some cases we might want to extend our application DI container for local needs.
@Composable
fun ContentView() {
subDI({ (1)
bindSingleton { PersonService() } (2)
}) {
ItemList() (3)
ActionView() (3)
}
}
1 | Extend the current DI from LocalDI |
2 | Add specific bindings for the underlying tree |
3 | every underlying @Composable element can access the bindings declared in the parent’s DI container + the local bindings added in 2. |
You can also extend an existing global DI container, like in the following example:
@Composable
fun ContentView() {
subDI(parentDI = globalDI, (1)
diBuilder = {
bindSingleton { PersonService() } (2)
}) {
ItemList() (3)
ActionView() (3)
}
}
1 | The DI container to extend |
2 | Add specific bindings for the underlying tree |
3 | every underlying @Composable element can access the bindings declared in the parent’s DI container + the local bindings added in 2. |
With this feature we can extend our DI container. This extension is made by copying the none singleton / multiton, but we have the possibility to copy all the binding (including singleton / multiton).
@Composable
fun ContentView() {
subDI(copy = Copy.All, (1)
diBuilder = {
/** new bindings / overrides **/
}) {
ItemList() (2)
ActionView() (2)
}
}
1 | Copying all the bindings, with the singletons / multitons |
2 | every underlying @Composable element can access the bindings declared in the parent’s DI container + the local bindings. |
By doing a Copy.All your original singleton / multiton won’t be available anymore, in the new DI container, they will exist as new instances.
|
Sometimes, It might be interesting to replace an existing dependency (by overriding it).
@Composable
fun App() = withDI({
bindProvider<Dice> { RandomDice(0, 5) }
bindSingleton<DataSource> { SqliteDS.open("path/to/file") }
}) {
MyView {
ContentView()
}
}
@Composable
fun ContentView() {
subDI(allowSilentOverrides = true, (1)
diBuilder = {
bindProvider<Dice> { RandomDice(0, 10) } (2)
}) {
ItemList() (3)
ActionView() (3)
}
}
1 | Overriding in the subDI will be implicit |
2 | Silently overrides the Dice provider define in an upper node |
3 | every underlying @Composable element can access the bindings declared in the parent’s DI container + the local bindings added in 2. |
Retrieve bindings from @Composable
functions
If you have defined a DI container in a LocalDI
, you can consider every underlying @Composable
as DI aware.
This means that can access the current DI container and its bindings with one of the following function delegates:
-
val t: TYPE by rememberInstance()
-
val f: (ARG_TYPE) → TYPE by rememberFactory()
-
val p: () → TYPE by rememberProvider()
If you are not familiar with these declarations you can explore the detailed documentation on bindings and injection/retrieval. |
Here are some examples on how to retrieve instances, factories or providers within a @Composable
function.
@Composable
fun ContentView() {
val dice: Dice by rememberInstance() (1)
}
@Composable
fun ContentView() {
val diceFactory: (Int) -> Dice by rememberFactory() (1)
}
@Composable
fun ContentView() {
val diceProvider: () -> Dice by rememberProvider() (1)
val personProvider: () -> Person by rememberProvider(arg = "Romain") (1)
}
Under the hood these functions are using LocalDI . If there is no DI container define in the @Composable current hierarchy, you will get a runtime exception: IllegalStateException: Missing DI container! .
|
Android specific usage
kodein-di-framework-compose
Android source set adds the transitive dependencies kodein-di
and kodein-di-framework-android-x
.
This gives us the ability to combine two important concepts that are DIAware
and the closest DI pattern.
It adds to some Android specific objects, an extension function di()
, that is capable of exploring the context hierarchy until it finds a DI container, hence the name of the pattern.
Thanks to these mechanisms we can provide two specifcs functions for Jetpack Compose users.
-
A
@Composable
functionandroidContextDI
that uses the closest DI pattern to get a DI container by using the CompositionLocalLocalContext
, from Jetpack Compose.
class MainActivity : ComponentActivity(), DIAware { (1)
override val di: DI = DI.lazy { (2)
bindSingleton<DataSource> { SqliteDS.open("path/to/file") }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
@Composable
fun App() {
val di = androidContextDI() (3)
val dataSource: DataSource by rememberInstance()
Text(text = "Hello ${dataSource.getUsername()}!")
}
1 | Your Android context must be DIAware … |
2 | … and override the di property. |
3 | the androidContextDI function retrieve the di property from the closest DIAware object.
|
This uses the androidContextDI
function to provide a DI container as the CompositionLocal LocalDI
.
class MainActivity : ComponentActivity(), DIAware { (1)
override val di: DI = DI.lazy { (2)
bindSingleton<DataSource> { SqliteDS.open("path/to/file") }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
@Composable
fun App() = withDI { (3)
MyContentView()
}
@Composable
fun MyContentView() {
val dataSource: DataSource by rememberInstance() (4)
Text(text = "Hello ${dataSource.getUsername()}!")
}
1 | Your Android context must be DIAware … |
2 | … and override the di property. |
3 | Add the closest DI container to the @Composable hierarchy |
4 | Underlying @Composable can transparently access to the DI container defined in the closest Android’s context. |