Skip to content

Dependency Injection in Kotlin Multiplatform (KMP) with Koin

Oussama Aniba
6 min read
Dependency Injection in Kotlin Multiplatform (KMP) with Koin

As a mobile developer Dependency Injection (DI) is a design pattern in software engineering where an object's dependencies are provided (or "injected") by an external entity rather than the object creating the dependencies itself. This is typically achieved through a constructor, setter, or interface injection. The main idea is to separate the creation of an object's dependencies from the object's behavior, promoting a more modular and testable codebase.

Let's start with some key fundamentals before digging into this topic. Still, it won't be long as many of you will be familiar with mobile development and dependency injection.

So why Koin?

Besides Koin supporting KMP environment, it provides an easy and efficient way to incorporate dependency injection into any Kotlin application (Multiplatform, Android, backend ...)

The goals of Koin are:

  • Simplify your Dependency Injection infrastructure with smart API
  • Kotlin DSL is easy to read, easy to use, to lets you write any kind of application
  • Provides different kinds of integration from the Android ecosystem, to more backend needs like Ktor
  • Allow to be used with annotations

AND THAT'S SOME KIND OF INFORMATION THAT YOU COULD FIND EASILY ON THE INTERNET.. I know it's boring...

Enough talk and let's start the exciting part where you can see the magic 😃

For this example, I'll be using PokéAPI to display a list of Pokemon's

First Start by creating your project over

Kotlin Multiplatform Wizard | JetBrains
Create your first multiplatform project using the Kotlin Multiplatform wizard for Android, iOS, and Desktop, or use one of the pre-made templates.

Leave the configuration as it is, only specify your project name and package name (Project ID), and then open it using your Android Studio (Mine is: Android Studio Koala | 2024.1.1)

After opening the project I cleaned the boilerplate code

let's start by importing packages and dependencies that we need, go to

gradle/libs.version.toml and add the following

Under [versions] add :

ktor-client = "2.3.11"
lifecycleViewModel = "2.8.2"

koin = "3.6.0-alpha3"
koinCompose = "3.6.0-alpha3"
koinComposeMultiplatform = "1.2.0-alpha3"
navigationCompose = "2.7.0-alpha03"

Under [libraries] add :

navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }

ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-client" }
ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor-client" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor-client" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor-client" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-client" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor-client" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor-client" }

koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinCompose" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinComposeMultiplatform" }

lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleViewModel"}
  

Under [plugins] add :

kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

Go to the APP level gradle file and add the following

First of all add under plugins

alias(libs.plugins.kotlin.serialization)

Under sourceSets > androidMain.dependencies :

implementation(libs.ktor.client.okhttp)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)

Under sourceSets > commonMain.dependencies :

implementation(libs.ktor.client.core)
implementation(libs.ktor.client.logging)            implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.content.negotiation)            implementation(libs.ktor.client.encoding)
api(libs.koin.core)
implementation(libs.koin.compose)            implementation(libs.navigation.compose)
implementation(libs.lifecycle.viewmodel)

and then add the IOS part as follows:

iosMain.dependencies {
     implementation(libs.ktor.client.darwin) // added
}

Hit sync now and wait, once finished without errors, let's make sure we keep in mind some key conditions in mind

Upon creating modules we first need to know about the expect/actual implementation

Rules for expected and actual declarations

To define expected and actual declarations, follow these rules:

  1. In the common source set, declare a standard Kotlin construct. This can be a function, property, class, interface, enumeration, or annotation.
  2. Mark this construct with the expect keyword. This is your expected declaration. These declarations can be used in the common code, but shouldn't include any implementation. Instead, the platform-specific code provides this implementation.
  3. In each platform-specific source set, declare the same construct in the same package and mark it with the actual keyword. This is your actual declaration, which typically contains an implementation using platform-specific libraries.

Now let's set what we need for HTTP calls, ViewModels, and Koin initialization

Under common main add the following files, preferably following the same architecture unless you know how to adapt the code based on your needs, the same structure must be added under androidMain and iosMain, and file nomination and path must be identical on all parts

[module] package folder contains the following:

[repository] package folder contains the following:

[source] package folder contains the following:

[models] package folder contains the following:

this data class has been generated using JSON to Kotlin class plugin in Android Studio, you can see the actual data that we will be using Pokémon API

Now let's see what's under the hood for androidMain and iosMain folder

Extra configuration (Android only)

Let's define our Application entry and initialize Koin first

under androidMain > kotlin > com.your.package-name > create a file/class PokeApplication and set-up the following

Add Internet permission and name attribute into AndroidManifest.xml

Hurraaaaaaah!! you made it, start your project, and hopefully, you won't face any issues. if so let's take a look inside our viewModel and display some quick information, nothing much just a list that shows what we got through the Poké API

PS: I didn't mind much about the design or the design pattern or even the architecture the sole reason for this article is to show you how things are done with KMP

I won't be explaining much about the UI or the business logic as I presume that if you are reading this article you know enough about Jetpack Compose principle and Kotlin

You can find the full source code through my GitHub Repository

Hope this can help you through your learning journey as I am still learning

Enjoy!

References:

Expected and actual declarations | Kotlin

THIS DUDE IS AWESOOOOOOOME !

KotlinKMPKotlin multiplatformAndroidIOSKoin dependency injectionDI