In this tutorial, we'll explore how to set up a Kotlin Multiplatform project targeting both JVM and Native platforms (
Linux, macOS, Windows) using Ktor for building a backend server.
Project Structure
The project follows a standard Kotlin Multiplatform structure:
-
settings.gradle.kts- Project settings -
build.gradle.kts- Build configuration with multiplatform setup -
gradle/libs.versions.toml- Centralized dependency version management
Version Catalog (libs.versions.toml)
The project uses Gradle's version catalog feature to manage dependencies centrally. Here's how it's configured:
File: gradle/libs.versions.toml
[versions]
kotlin = "2.2.21"
ktor = "3.3.3"
koin = "4.1.0"
amqp = "0.4.0"
flareon = "0.1.1"
logback = "1.5.18"
[plugins]
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
[libraries]
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
amqp = { module = "dev.kourier:amqp-client-robust", version.ref = "amqp" }
flareon-messaging = { module = "digital.guimauve.flareon:messaging", version.ref = "flareon" }
logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
This centralized approach makes it easy to update versions across the entire project.
Build Configuration (build.gradle.kts)
File: build.gradle.kts
plugins {
alias(libs.plugins.multiplatform)
alias(libs.plugins.serialization)
}
group = "me.nathanfallet.ktornativeworkertutorial"
version = "1.0"
repositories {
mavenCentral()
}
kotlin {
// JVM target for development and testing
jvm {
mainRun {
mainClass.set("me.nathanfallet.ktornativeworkertutorial.ApplicationKt")
}
}
// Native targets for production deployment
listOf(
linuxX64(),
macosArm64(),
mingwX64()
).forEach {
it.binaries {
executable {
entryPoint = "me.nathanfallet.ktornativeworkertutorial.main"
}
}
}
sourceSets {
commonMain.dependencies {
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.cio)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.koin.core)
implementation(libs.koin.ktor)
implementation(libs.amqp)
implementation(libs.flareon.messaging)
}
jvmMain.dependencies {
// Required for logging to appear on JVM
implementation(libs.logback.core)
implementation(libs.logback.classic)
}
}
}
Key Configuration Points
-
Plugins:
-
multiplatform: Enables Kotlin Multiplatform support -
serialization: Enables Kotlinx Serialization for JSON handling
-
-
JVM Target:
- Configured with
mainRunto specify the main class - Useful for development and testing
- Entry point:
me.nathanfallet.ktornativeworkertutorial.ApplicationKt
- Configured with
-
Native Targets:
- Linux x64 (
linuxX64()) - macOS ARM64 (
macosArm64()) - Windows x64 (
mingwX64()) - Each configured with an executable binary
- Entry point:
me.nathanfallet.ktornativeworkertutorial.main
- Linux x64 (
-
Source Sets:
-
commonMain: Dependencies shared across all platforms -
jvmMain: JVM-specific dependencies (Logback for logging)
-
Project Settings (settings.gradle.kts)
File: settings.gradle.kts
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "ktor-native-worker-tutorial"
This configuration enables automatic JDK provisioning through the Foojay toolchain resolver.
Platform-Specific Code with expect/actual
To handle platform-specific implementations (like reading environment variables or files), the project uses Kotlin's
expect/actual mechanism.
Common Interface (src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/Env.kt):
expect fun getEnv(name: String): String?
expect fun readFile(path: String): String
JVM Implementation (src/jvmMain/kotlin/me/nathanfallet/ktornativeworkertutorial/Env.jvm.kt):
actual fun getEnv(name: String): String? {
return System.getenv(name)
}
actual fun readFile(path: String): String {
return java.io.File(path).readText()
}
Native Implementation (src/nativeMain/kotlin/me/nathanfallet/ktornativeworkertutorial/Env.native.kt):
@OptIn(ExperimentalForeignApi::class)
actual fun getEnv(name: String): String? {
return getenv(name)?.toKString()
}
@OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
actual fun readFile(path: String): String {
val file = fopen(path, "r") ?: throw IllegalArgumentException("Cannot open file: $path")
try {
fseek(file, 0, SEEK_END)
val size = ftell(file)
fseek(file, 0, SEEK_SET)
return buildString {
val buffer = ByteArray(1024)
while (true) {
val read = fread(buffer.refTo(0), 1u, buffer.size.toULong(), file).toInt()
if (read <= 0) break
append(buffer.decodeToString(0, read))
}
}
} finally {
fclose(file)
}
}
This approach allows the same common code to work across all platforms while using platform-specific APIs under the
hood.
Running the Project
With this Gradle setup, you can run the project on different platforms:
# JVM (for development/testing)
./gradlew jvmRun
# Native platforms
./gradlew runDebugExecutableMacosArm64 # macOS
./gradlew runDebugExecutableLinuxX64 # Linux
./gradlew runDebugExecutableMingwX64 # Windows
Building Native Executables
To build optimized native executables for production:
./gradlew build
The executables will be located in build/bin/{platform}/releaseExecutable/.
Summary
This Gradle setup provides:
- Multiplatform support (JVM + Native)
- Centralized dependency management via version catalogs
- Platform-specific implementations using expect/actual
- Easy development with JVM and production deployment with native binaries
- Clean code organization following Kotlin conventions
In the next part, we'll explore how to implement notification sending using Firebase Cloud Messaging.
Top comments (0)