Kodein-DI and Compose (Android, Desktop, or JS)

You can use Kodein-DI as-is in your Android / Desktop / JS 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 Compose compiler Kotlin

7.19.0

Compose 1.3.1

1.8.10

7.18.0

Compose 1.3.0-rc2

1.8.0

7.17.1

Compose 1.3.0-rc2

1.8.0

7.16.0

Compose 1.2.0

1.7.20

7.15.1

Compose 1.2.0

1.7.20

7.15.0

NOT COMPATIBLE

1.7.20

7.15.0-kotlin-1.7.20-RC

NOT COMPATIBLE

1.7.20

7.14.0

1.2.0-alpha01-dev745

1.7.10

7.13.1

1.2.0-alpha01-dev745

1.7.0

7.10.0

1.0.1-rc2

1.6.10

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, Desktop, or JavaScript projects with the same approach (thanks to Gradle’s metadata).

Start by adding the correct dependency to your Gradle build script:

Gradle Groovy script
implementation 'org.kodein.di:kodein-di-framework-compose:7.19.0'
Gradle Kotlin script
implementation("org.kodein.di:kodein-di-framework-compose:7.19.0")

Using kodein-di-framework-compose, whatever your platform target, will transitively add the Kodein-DI core module kodein-di to your dependencies.

On Android it will transitively add the specific module kodein-di-framework-android-x (see Android modules).

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.

sharing a DI container within a @Composable tree
val 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
Creating a DI container with DI modules within a @Composable tree
val 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
Creating a DI container and expose it to a @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 is 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 function localDI().

Getting the DI container from parent nodes
@Composable
fun ContentView() {
    val di = localDI() (1)
    val dice: Dice by di.instance() (2)
}
1 Get the DI container attached 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.

Extend a DI container from the Compose context
@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:

Extend a DI container from its reference
@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.
Copying bindings

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).

Example: Copying all the bindings
@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.
Overriding bindings

Sometimes, It might be interesting to replace an existing dependency (by overriding it).

Example: overriding bindings
@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 they can access the current DI container and its bindings with one of the following function delegates:

Retrieve instances
@Composable
fun ContentView() {
    val dice: Dice by rememberDI { instance() }
}

rememberDI allows you to remember the reference of an instance retrieved from a DI container.

Under the hood, rememberDI { } uses the localDI() function. If there is no DI container defined in the @Composable current hierarchy, you will get a runtime exception, i.e. IllegalStateException: Missing DI container!.

If you need a specific interaction with the DI container, in a @Composable tree, you can use rememberDI { } to wrap your implementation. Following you can find wrappers already provided by Kodein-DI.

a wrapper for rememberDI { instance() }
@Composable
fun ContentView() {
    val dice: Dice by rememberInstance()
}
a wrapper for rememberDI { named.instance() }
@Composable
fun ContentView() {
    val dice: Dice by rememberInstance(tag = "dice")
    // is the same as...
    val dice: Dice by rememberNamedInstance()
}
a wrapper for rememberDI { factory() }
@Composable
fun ContentView() {
    val diceFactory: (Int) -> Dice by rememberFactory()
}
a wrapper for rememberDI { provider() }
@Composable
fun ContentView() {
    val diceFactory: (Int) -> Dice by rememberFactory()
}
If you are not familiar with these declarations you can explore the detailed documentation on bindings and injection/retrieval.
Retrieve providers
@Composable
fun ContentView() {
    val diceProvider: () -> Dice by rememberProvider()
}
the rememberX functions will preserve the retrieved instance on every composition.

Android specific usage

On kodein-di-framework-compose the Android source set adds the transitive dependencies to 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.

TL;DR - It helps us adds to some Android specific objects, an extension function closestDI(), 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, to Jetpack Compose users, a @Composable function androidContextDI that uses the closest DI pattern to get a DI container by using the CompositionLocal.

With that, any @Composable can retrieve instances from the DI container as long as they can access the upper bound DIAware (i.e. Activity or Fragment).
Getting the closest DI context from the Android’s context
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 dataSource: DataSource by rememberInstance() (3)
    Text(text = "Hello ${dataSource.getUsername()}!")
}
1 Your Android context must be DIAware …​
2 …​ and override the di property.
3 Uses the androidContextDI function to retrieve the di property from the closest DIAware object.