With the rise of Kotlin Multiplatform (KMP), it gave birth to a cross-platform UI library, Compose Multiplatform (CMP). CMP is currently quite fresh out in the market. At the time of writing this article, it is still beta for iOS applications and alpha for the web (mainly because Kotlin/Wasm is still alpha).
While working on a current project in CMP, I researched and experiment with various Dependency Injection (and Service locator) libraries and kodein-di
stood out to me as a clear winner when it comes to their integration in CMP projects. I choose this library because:
- Easy dependencies — Unlike other libraries, the setup was simple as installing a single dependency that worked in all aspects.
- Great documentation — Although some sections of the documentation were confusing, my basic needs were described in detail.
- Simple constructs — The process of defining and retrieving dependencies had a natural flow that Dagger Hilt provided during native development.
The context
A project with CMP changes how a KMP project is structured. And for those who don’t understand dependency injection completely, I have a couple important definitions that you should be familiar with.
Folder structure
|- composeApp/ # This is UI module for sharing UI code between platforms
| |- src/
| |- build.gradle.kts
|- gradle/
|- iosApp/
|- server/
|- shared/ # Shared business logic (KMP)
| |- src/
| |- build.gradle.kts
Bindings
Bindings can be understood as a mapping of a type to the actual object. E.g. I could bind all String
s to "Look dad, DI!"
.
DI Container/Module/Instance
The documentation on kodein-di uses these terms interchangeably. They are a collection of bindings. Later you would see that we can export and extend DI modules with kodein-di
and use it similar to Object-Oriented Programming. Would this be called “Module-Object Programming”? 🤔
Singletons
Within a DI container, requesting an instance will always return the same object. Singletons are lazy so they would never be created if the application never asked for it.
Providers & Factories
Within a DI container, every time you request for an instance of a type, it will return a new object. Providers don’t accept any parameters and their function signature is () -> T
. Factories can pass in a single parameter to create a dependency on every instance request. They have a function signature of (A) -> T
.
Dependency Injection in CMP Project
If you don’t understand the previous definitions fully, you might understand them with some code samples. First, we should implement the dependency.
Installing Kodein-DI for Compose
In your gradle/libs.versions.toml
,
[versions]
kodeinDiCompose = "7.22.0"
[libraries]
kodein-di-compose = { module = "org.kodein.di:kodein-di-framework-compose", version.ref = "kodeinDiCompose" }
And you would require to add the dependency in the commonMain
sourceSet of composeApp
and shared
modules.
// In both composeApp/build.gradle.kts
// and shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kodein.di.compose)
}
}
}
Sync the project and allow Gradle to download the repository from upstream remote.
Create your App-level Module
Coming from a background of using Dagger Hilt in native android development, I like having a global dependency container to hoist dependencies the entire app should use.
Create a file shared/src/commonMain/kotlin/AppModule.kt
and add the following code:
import org.kodein.di.DI
val appModule = DI {}
Invoking the DI
function creates a DI container to work in. To add a binding to this container, we use the bind function supplying the type I want to bind it to:
val appModule = DI {
bind<Repository> { /* see below */ }
}
// Assume that this exists:
interface Repository
Within the lambda function, you have to specify the type of binding you want to provide i.e. a singleton, provider or factory. For the sake of demonstration, let’s provide a singleton:
val appModule = DI {
bind<Repository> { singleton { RepositoryImpl() } }
}
// Assume that this exists:
interface Repository
class RepositoryImpl : Repository
Awesome! We created our first dependency inside a DI container. You shorten the bind { singleton {} }
line to bindSingleton {}
:
val appModule = DI {
bindSingleton<Repository> { RepositoryImpl() }
}
// And other functions include:
val appModule = DI {
bindSingleton<Repository> { RepositoryImpl() }
bindProvider<Repository> { ReposioryImpl() }
bindFactory<Params, Repository> { params -> RepositoryImpl(params) }
}
Job Offers
Using dependencies in CMP
Oftentimes, you would have a ViewModel
associated with a screen drawn with Compose. Previously, you write some code such as:
@Composable
fun HomeScreen() {
val viewModel = viewModel { HomeViewModel(RepositoryImpl()) }
Text(text = viewModel.text)
}
// Assume that this exists:
class HomeViewModel(
private val repository: Repository
) : ViewModel()
With kodein-di-framework-compose
, dealing with Dependency Injection within composable functions is easy.
import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.subDI
@Composable
fun HomeScreen() {
subDI(
parentDi = appModule // from AppModule.kt above
diBuilder = {
bindSingleton { HomeViewModel(instance()) }
}
) {
val viewModel by rememberInstance<HomeViewModel>()
Text(text = viewModel.text)
}
}
// Assume that this exists:
class HomeViewModel(
private val repository: Repository
) : ViewModel()
Breaking down the code happening above:
subDI
is a function that extends any local DI container for the given Composable function. Here, we extend fromappModule
diBuilder
is a lambda function supplied tosubDI
that attaches any additional local binding to the container.instance
is where we are retrieving theRepository
type. But how is that happening? We know thatHomeViewModel
requires a type ofRepository
. So the functioninstance
inferred thatRepository
is needed to be returned.instance
looks for a binding ofRepository
in the container. The container extendsappModule
where the binding is defined. It returns the dependency toHomeViewModel
and binds it.rememberInstance
grabs the instance we are looking for inside the Composable, explicitly supplying the type required.
Conclusion
Et Voilà! You have implemented Dependency Injection in one of the simplest way. There are many features that kodein-di
and kodein-di-framework-compose
has to offer. Their documentation is fantastic so do check it out:
Getting started with Kodein-DI
Edit description
kosi-libs.org
Kodein-DI and Compose (Android, Desktop, or JS)
You can use as-is in your Android / Desktop / JS project, but you can level-up your game by using the library…
kosi-libs.org
Thank you for reading if you made it this far. Consider following me to read more articles from me regarding Kotlin, Nuxt3 and general programming practices.
Want to connect?
GitHub profile
Portfolio website
This article is previously published on proandroiddev.com