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:

Gradle Groovy script
implementation 'org.kodein.di:kodein-di-framework-compose:7.7.0'
Gradle Kotlin script
implementation("org.kodein.di:kodein-di-framework-compose:7.7.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 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 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.

Getting the DI container from parent nodes
@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.

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

Retrieve instances
@Composable
fun ContentView() {
    val dice: Dice by rememberInstance() (1)
}
Retrieve factories
@Composable
fun ContentView() {
    val diceFactory: (Int) -> Dice by rememberFactory() (1)
}
Retrieve providers
@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.

  1. A @Composable function androidContextDI that uses the closest DI pattern to get a DI container by using the CompositionLocal LocalContext, from Jetpack Compose.

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 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.
  • A specific version of the withDI

This uses the androidContextDI function to provide a DI container as the CompositionLocal LocalDI.

Android 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() = 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.