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.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:
implementation 'org.kodein.di:kodein-di-framework-compose:7.16.0'
implementation("org.kodein.di:kodein-di-framework-compose:7.16.0")
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 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()
.
@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.
@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 they can access the current DI container and its bindings with one of the following function delegates:
@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.
rememberDI { instance() }
@Composable
fun ContentView() {
val dice: Dice by rememberInstance()
}
rememberDI { named.instance() }
@Composable
fun ContentView() {
val dice: Dice by rememberInstance(tag = "dice")
// is the same as...
val dice: Dice by rememberNamedInstance()
}
rememberDI { factory() }
@Composable
fun ContentView() {
val diceFactory: (Int) -> Dice by rememberFactory()
}
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. |
@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).
|
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. |