DEV Community

Nathan Fallet
Nathan Fallet

Posted on

Part 1: Gradle Setup - Ktor native worker tutorial

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" }
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Configuration Points

  1. Plugins:

    • multiplatform: Enables Kotlin Multiplatform support
    • serialization: Enables Kotlinx Serialization for JSON handling
  2. JVM Target:

    • Configured with mainRun to specify the main class
    • Useful for development and testing
    • Entry point: me.nathanfallet.ktornativeworkertutorial.ApplicationKt
  3. Native Targets:

    • Linux x64 (linuxX64())
    • macOS ARM64 (macosArm64())
    • Windows x64 (mingwX64())
    • Each configured with an executable binary
    • Entry point: me.nathanfallet.ktornativeworkertutorial.main
  4. 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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Building Native Executables

To build optimized native executables for production:

./gradlew build
Enter fullscreen mode Exit fullscreen mode

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)