Master Kotlin Multiplatform with Decompose — Part 2: Dependency Injection, Kodein, Koin
Let’s enhance our app by adding a Dependency Injection (DI) container.
This article is part of a series on Kotlin Multiplatform with Decompose. If you haven’t read the previous article, I recommend starting there.
This article covers:
- Preparing code for dependency injection.
- Manually injecting dependencies.
- Injecting dependencies with Kodein.
- Injecting dependencies with Koin.
Preparing code for dependency injection
Our components will have two types of dependencies:
- Component creation and interaction dependencies (e.g., componentContext, IDs, lambdas) should be passed from the parent component using a factory method.
- Information and business logic dependencies (e.g., interactors, use cases, repositories) can be injected.
Create a Repository interface and its implementation
Previously, we stored the list of posts within the component. Now, let’s move them to the Repository class.
interface PostRepository {
fun getAllPosts(): List<Post>
fun getPost(id: String): Post
}
class DefaultPostRepository : PostRepository {
private val posts = List(16) {
Post(
id = it.toString(),
title = "Title-#$it",
description = "Description-#$it",
author = "Author-#$it",
)
}
override fun getAllPosts(): List<Post> = posts
override fun getPost(id: String): Post = posts.first { it.id == id }
}
Create a Factory interface for creating DetailComponent
interface DetailComponent {
val model: Value<Post>
fun onBackPressed()
// 1
fun interface Factory {
operator fun invoke( // 2
componentContext: ComponentContext,
postId: String,
onFinished: () -> Unit,
): DetailComponent
}
}
- Create a Factory SAM interface with the sole responsibility of component creation.
- The invoke method will create the DetailComponent, accepting only dependencies from the parent component as parameters.
Create a Factory class for DefaultDetailComponent
class DefaultDetailComponent(
componentContext: ComponentContext,
postId: String, // 1
private val repository: PostRepository, // 2
private val onFinished: () -> Unit,
) : DetailComponent, ComponentContext by componentContext {
// 3
override val model: Value<Post> = MutableValue(repository.getPost(postId))
override fun onBackPressed() = onFinished()
// 4
class Factory(
private val repository: PostRepository,
) : DetailComponent.Factory {
override fun invoke(
componentContext: ComponentContext,
postId: String,
onFinished: () -> Unit,
): DetailComponent = DefaultDetailComponent( // 5
componentContext = componentContext,
postId = postId,
onFinished = onFinished,
repository = repository,
)
}
}
- To get detailed information about a post, we only need the postId.
- Declare the repository dependency.
- Retrieve the post from the repository using postId.
- Create a Factory class for DefaultDetailComponent, passing the repository through the constructor to the component.
Create a Factory interface for ListComponent creation
interface ListComponent {
val model: Value<List<Post>>
fun onPostClicked(post: Post)
// 1
fun interface Factory {
operator fun invoke(
componentContext: ComponentContext,
postClicked: (postId: String) -> Unit,
): ListComponent
}
}
- Create a Factory SAM interface for ListComponent, similar to the one for DetailComponent.
Create a Factory class for DefaultListComponent
class DefaultListComponent(
componentContext: ComponentContext,
repository: PostRepository, // 1
private val postClicked: (postId: String) -> Unit, // 2
) : ListComponent, ComponentContext by componentContext {
// 3
override val model: Value<List<Post>> = MutableValue(repository.getAllPosts())
override fun onPostClicked(post: Post) = postClicked(post.id)
// 4
class Factory(
private val repository: PostRepository
) : ListComponent.Factory {
override fun invoke(
componentContext: ComponentContext,
postClicked: (postId: String) -> Unit
): ListComponent {
return DefaultListComponent(
componentContext = componentContext,
postClicked = postClicked,
repository = repository,
)
}
}
}
- Declare the repository dependency.
- Pass post click handling to the parent component using only postId.
- Retrieve all posts from the repository.
- Create a Factory class for DefaultListComponent.
Create a Factory interface for RootComponent creation
interface RootComponent {
val stack: Value<ChildStack<*, Child>>
sealed interface Child {
class List(val component: ListComponent) : Child
class Detail(val component: DetailComponent) : Child
}
fun interface Factory {
operator fun invoke(componentContext: ComponentContext): RootComponent
}
}
Finally, create a Factory class for DefaultRootComponent
class DefaultRootComponent( // 1
componentContext: ComponentContext,
private val listComponentFactory: ListComponent.Factory,
private val detailComponentFactory: DetailComponent.Factory,
) : RootComponent, ComponentContext by componentContext {
private val nav = StackNavigation<Config>()
override val stack: Value<ChildStack<*, RootComponent.Child>> = childStack(
source = nav,
serializer = Config.serializer(),
initialConfiguration = Config.List,
handleBackButton = true,
childFactory = ::child,
)
private fun child(
config: Config,
componentContext: ComponentContext
): RootComponent.Child = when (config) {
Config.List -> RootComponent.Child.List(
listComponentFactory( // 2
componentContext = componentContext,
postClicked = { postId -> nav.pushNew(Config.Detail(postId)) }
)
)
is Config.Detail -> RootComponent.Child.Detail(
detailComponentFactory( // 3
componentContext = componentContext,
postId = config.postId,
onFinished = { nav.pop() },
)
)
}
@Serializable
private sealed interface Config {
@Serializable
data object List : Config
@Serializable
data class Detail(val postId: String) : Config // 4
}
// 5
class Factory(
private val listComponentFactory: ListComponent.Factory,
private val detailComponentFactory: DetailComponent.Factory,
) : RootComponent.Factory {
override fun invoke(componentContext: ComponentContext): RootComponent {
return DefaultRootComponent(
listComponentFactory = listComponentFactory,
detailComponentFactory = detailComponentFactory,
componentContext = componentContext,
)
}
}
}
- Receive ListComponent.Factory dependency to create ListComponent and DetailComponent.Factory to create DetailComponent.
- Create ListComponent using the factory’s invoke method.
- And DetailComponent similarly.
- Update Config.Detail to accept postId instead of the entire post.
- Create a Factory class for DefaultRootComponent.
Now our components are ready for injection. 😎
Manually injecting dependencies
Create a dependencies holder class
object DependencyInjection {
val repository: PostRepository = DefaultPostRepository()
val detailComponentFactory: DetailComponent.Factory = DefaultDetailComponent.Factory(
repository = repository
)
val listComponentFactory: ListComponent.Factory = DefaultListComponent.Factory(
repository = repository,
)
val rootComponentFactory: RootComponent.Factory = DefaultRootComponent.Factory(
detailComponentFactory = detailComponentFactory,
listComponentFactory = listComponentFactory,
)
}
Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1
val rootComponentFactory = DependencyInjection.rootComponentFactory
// Always create the root component outside Compose on the main thread
// 2
val rootComponent = rootComponentFactory(defaultComponentContext())
setContent {
App(rootComponent = rootComponent)
}
}
}
- Receive the RootComponent.Factory dependency from our DependencyInjection.
- Create RootComponent using the factory interface’s invoke function.
iOS
fun MainViewController() = ComposeUIViewController {
val rootComponent = remember {
val rootComponentFactory = DependencyInjection.rootComponentFactory
rootComponentFactory(DefaultComponentContext(ApplicationLifecycle()))
}
App(rootComponent = rootComponent)
}
Desktop
fun main() {
val lifecycle = LifecycleRegistry()
val rootComponentFactory = DependencyInjection.rootComponentFactory
val rootComponent = runOnUiThread {
rootComponentFactory(
componentContext = DefaultComponentContext(lifecycle),
)
}
application {
val windowState = rememberWindowState()
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "Decompose Quick Guide",
) {
LifecycleController(
lifecycleRegistry = lifecycle,
windowState = windowState,
windowInfo = LocalWindowInfo.current,
)
App(rootComponent = rootComponent)
}
}
}
Web
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val lifecycle = LifecycleRegistry()
val rootComponentFactory = DependencyInjection.rootComponentFactory
val rootComponent = rootComponentFactory(DefaultComponentContext(lifecycle))
lifecycle.attachToDocument()
ComposeViewport(document.body!!) {
App(rootComponent = rootComponent)
}
}
Injecting dependencies with Kodein
Add the dependency
- First, declare the version and library of Kodein in the libs.versions.toml file.
[versions]
...
kodein-di = "7.22.0"
[libraries]
...
kodein-di = { module = "org.kodein.di:kodein-di", version.ref = "kodein-di" }
2. Update build.gradle.kts inside the composeApp folder.
...
kotlin {
...
sourceSets {
...
commonMain.dependencies {
...
implementation(libs.kodein.di)
}
}
}
3. Finally, sync Gradle changes.
Create a DI container
val kodeinDI = DI {
bindSingleton<PostRepository> { DefaultPostRepository() }
bindSingleton<DetailComponent.Factory> {
DefaultDetailComponent.Factory(
repository = instance(),
)
}
bindSingleton<ListComponent.Factory> {
DefaultListComponent.Factory(
repository = instance(),
)
}
bindSingleton<RootComponent.Factory> {
DefaultRootComponent.Factory(
detailComponentFactory = instance(),
listComponentFactory = instance(),
)
}
}
- Here we’re simply binding the interface to its concrete implementation.
Now we can inject dependencies. 😎
Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootComponentFactory: RootComponent.Factory by kodeinDI.instance()
// Always create the root component outside Compose on the main thread
val rootComponent = rootComponentFactory(defaultComponentContext())
setContent {
App(rootComponent = rootComponent)
}
}
}
iOS
fun MainViewController() = ComposeUIViewController {
val rootComponent = remember {
val rootComponentFactory: RootComponent.Factory by kodeinDI.instance()
rootComponentFactory(DefaultComponentContext(ApplicationLifecycle()))
}
App(rootComponent = rootComponent)
}
Desktop
fun main() {
val lifecycle = LifecycleRegistry()
val rootComponentFactory: RootComponent.Factory by kodeinDI.instance()
val rootComponent = runOnUiThread {
rootComponentFactory(
componentContext = DefaultComponentContext(lifecycle),
)
}
application {
val windowState = rememberWindowState()
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "Decompose Quick Guide",
) {
LifecycleController(
lifecycleRegistry = lifecycle,
windowState = windowState,
windowInfo = LocalWindowInfo.current,
)
App(rootComponent = rootComponent)
}
}
}
Web
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val lifecycle = LifecycleRegistry()
val rootComponentFactory: RootComponent.Factory by kodeinDI.instance()
val rootComponent = rootComponentFactory(DefaultComponentContext(lifecycle))
lifecycle.attachToDocument()
ComposeViewport(document.body!!) {
App(rootComponent = rootComponent)
}
}
Injecting dependencies with Koin
Add dependency
- Update the libs.versions.toml file.
[versions]
...
koin = "3.5.6"
[libraries]
...
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
2. Update build.gradle.kts inside the composeApp folder.
...
kotlin {
...
sourceSets {
...
commonMain.dependencies {
...
implementation(libs.koin.core)
}
}
}
Create a DI container.
val koin by lazy { initKoin().koin } // 1
// 2
fun initKoin(appDeclaration: KoinAppDeclaration? = null) = startKoin {
appDeclaration?.invoke(this)
modules(appModule)
}
// 3
val appModule = module {
single<PostRepository> { DefaultPostRepository() }
single<DetailComponent.Factory> {
DefaultDetailComponent.Factory(
repository = get(),
)
}
single<ListComponent.Factory> {
DefaultListComponent.Factory(
repository = get(),
)
}
single<RootComponent.Factory> {
DefaultRootComponent.Factory(
detailComponentFactory = get(),
listComponentFactory = get(),
)
}
}
- A variable for the Koin instance.
- A function to start Koin. If you have platform-specific dependencies, call this method instead of using the Koin instance above. You also need an extra dependency to do that. Remember, Koin can only be started once; attempting to start it again will crash the app.
- Create our app module.
Now we can inject dependencies. 😎
Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootComponentFactory: RootComponent.Factory by koin.inject()
// Always create the root component outside Compose on the main thread
val rootComponent = rootComponentFactory(defaultComponentContext())
setContent {
App(rootComponent = rootComponent)
}
}
}
iOS
fun MainViewController() = ComposeUIViewController {
val rootComponent = remember {
val rootComponentFactory: RootComponent.Factory by koin.inject()
rootComponentFactory(DefaultComponentContext(ApplicationLifecycle()))
}
App(rootComponent = rootComponent)
}
Desktop
fun main() {
val lifecycle = LifecycleRegistry()
val rootComponentFactory: RootComponent.Factory by koin.inject()
val rootComponent = runOnUiThread {
rootComponentFactory(
componentContext = DefaultComponentContext(lifecycle),
)
}
application {
val windowState = rememberWindowState()
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "Decompose Quick Guide",
) {
LifecycleController(
lifecycleRegistry = lifecycle,
windowState = windowState,
windowInfo = LocalWindowInfo.current,
)
App(rootComponent = rootComponent)
}
}
}
Web
Unfortunately, I can’t currently find a solution for Web/Wasm Koin injection with Decompose. Although there are Koin versions that support WebAssembly, they are still in alpha and may encounter runtime errors in this example. 😞
Conclusion
Congratulations! 🎉 You’ve integrated DI libraries into your KMP project with Decompose! The source code for Manual DI, Kodein, and Koin is on GitHub.
What’s next?
There’s room for improvement. Stay tuned for upcoming articles on Decompose and other enhancements.
Other Parts:
- Master Kotlin Multiplication navigation with Decompose — Part 1
- Master Kotlin Multiplatform with Decompose — Part 3: Restoring state with InstanceKeeper and StateKeeper
- Master Kotlin Multiplatform with Decompose — Part 4: MVI
Connect
Let’s connect on LinkedIn and subscribe for updates!
Thank you!