Kotlin - Gradle Build

Overview

Gradle is the primary build tool for Kotlin projects, offering powerful dependency management, build configuration, and integration with the Kotlin ecosystem. This tutorial covers Kotlin DSL, project setup, dependency management, and advanced build configurations.

๐ŸŽฏ Learning Objectives:
  • Understand Gradle basics and Kotlin integration
  • Learn Kotlin DSL for build scripts
  • Master dependency management in Kotlin projects
  • Configure builds for different project types
  • Apply advanced Gradle features and optimizations

Getting Started with Gradle and Kotlin

Basic Project Structure

my-kotlin-project/
โ”œโ”€โ”€ build.gradle.kts          # Main build script (Kotlin DSL)
โ”œโ”€โ”€ settings.gradle.kts       # Project settings
โ”œโ”€โ”€ gradle.properties         # Gradle properties
โ”œโ”€โ”€ gradlew                   # Gradle wrapper script (Unix)
โ”œโ”€โ”€ gradlew.bat              # Gradle wrapper script (Windows)
โ”œโ”€โ”€ gradle/
โ”‚   โ””โ”€โ”€ wrapper/
โ”‚       โ”œโ”€โ”€ gradle-wrapper.jar
โ”‚       โ””โ”€โ”€ gradle-wrapper.properties
โ””โ”€โ”€ src/
    โ””โ”€โ”€ main/
        โ””โ”€โ”€ kotlin/
            โ””โ”€โ”€ Main.kt

Basic build.gradle.kts

// build.gradle.kts
plugins {
    kotlin("jvm") version "1.9.10"
    application
}

group = "com.example"
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib"))
    testImplementation(kotlin("test"))
}

application {
    mainClass.set("MainKt")
}

tasks.test {
    useJUnitPlatform()
}

kotlin {
    jvmToolchain(17)
}

settings.gradle.kts

// settings.gradle.kts
rootProject.name = "my-kotlin-project"

// Enable Gradle version catalogs (optional)
dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from(files("gradle/libs.versions.toml"))
        }
    }
}

Kotlin DSL Fundamentals

Basic Syntax and Configuration

// build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.9.10"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10"
}

// Project properties
group = "com.example.myapp"
version = "1.0.0-SNAPSHOT"
description = "My Kotlin Application"

// Java/Kotlin version configuration
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

// Configure Kotlin compilation
tasks.withType {
    kotlinOptions {
        jvmTarget = "17"
        freeCompilerArgs = listOf(
            "-Xjsr305=strict",
            "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
        )
    }
}

// Configure repositories
repositories {
    mavenCentral()
    google() // For Android libraries
    gradlePluginPortal() // For Gradle plugins
}

Dependency Management

// build.gradle.kts
dependencies {
    // Kotlin standard library
    implementation(kotlin("stdlib"))
    implementation(kotlin("reflect"))
    
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.3")
    
    // Serialization
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
    
    // HTTP client
    implementation("io.ktor:ktor-client-core:2.3.4")
    implementation("io.ktor:ktor-client-cio:2.3.4")
    implementation("io.ktor:ktor-client-content-negotiation:2.3.4")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.4")
    
    // Testing
    testImplementation(kotlin("test"))
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
    testImplementation("io.mockk:mockk:1.13.7")
    
    // Runtime dependencies
    runtimeOnly("ch.qos.logback:logback-classic:1.4.11")
}

Dependency Configuration Types

Understanding Configuration Scopes

dependencies {
    // Implementation - visible to this module only
    implementation("com.squareup.okhttp3:okhttp:4.11.0")
    
    // API - exposed to consumers of this module
    api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    
    // Compile only - needed for compilation but not runtime
    compileOnly("javax.annotation:javax.annotation-api:1.3.2")
    
    // Runtime only - needed at runtime but not compilation
    runtimeOnly("mysql:mysql-connector-java:8.0.33")
    
    // Test dependencies
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    
    // Annotation processors
    kapt("com.google.dagger:dagger-compiler:2.48")
    
    // Platform/BOM dependencies
    implementation(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
}

Version Catalogs

# gradle/libs.versions.toml
[versions]
kotlin = "1.9.10"
coroutines = "1.7.3"
ktor = "2.3.4"
junit = "5.10.0"

[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

[bundles]
ktor-client = ["ktor-client-core", "ktor-client-cio"]
testing = ["junit-jupiter", "coroutines-test"]

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
// build.gradle.kts using version catalog
plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.serialization)
}

dependencies {
    implementation(libs.kotlin.stdlib)
    implementation(libs.coroutines.core)
    implementation(libs.bundles.ktor.client)
    
    testImplementation(libs.bundles.testing)
}

Multi-Module Projects

Project Structure

my-multi-module-project/
โ”œโ”€โ”€ build.gradle.kts
โ”œโ”€โ”€ settings.gradle.kts
โ”œโ”€โ”€ core/
โ”‚   โ””โ”€โ”€ build.gradle.kts
โ”œโ”€โ”€ api/
โ”‚   โ””โ”€โ”€ build.gradle.kts
โ”œโ”€โ”€ web/
โ”‚   โ””โ”€โ”€ build.gradle.kts
โ””โ”€โ”€ app/
    โ””โ”€โ”€ build.gradle.kts

Root build.gradle.kts

// Root build.gradle.kts
plugins {
    kotlin("jvm") version "1.9.10" apply false
    kotlin("plugin.serialization") version "1.9.10" apply false
}

allprojects {
    group = "com.example.myapp"
    version = "1.0.0"
    
    repositories {
        mavenCentral()
    }
}

subprojects {
    apply(plugin = "org.jetbrains.kotlin.jvm")
    
    dependencies {
        testImplementation(kotlin("test"))
    }
    
    tasks.withType {
        kotlinOptions {
            jvmTarget = "17"
            freeCompilerArgs = listOf("-Xjsr305=strict")
        }
    }
    
    tasks.withType {
        useJUnitPlatform()
    }
}

Module Dependencies

// api/build.gradle.kts
dependencies {
    implementation(project(":core"))
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}

// web/build.gradle.kts
dependencies {
    implementation(project(":api"))
    implementation(project(":core"))
    implementation("io.ktor:ktor-server-netty:2.3.4")
    implementation("io.ktor:ktor-server-content-negotiation:2.3.4")
}

// app/build.gradle.kts
plugins {
    application
}

dependencies {
    implementation(project(":web"))
    implementation(project(":api"))
    implementation(project(":core"))
}

application {
    mainClass.set("com.example.myapp.MainKt")
}

Build Tasks and Configuration

Custom Tasks

// build.gradle.kts
import org.gradle.api.tasks.testing.logging.TestExceptionFormat

// Custom task to generate build info
tasks.register("generateBuildInfo") {
    description = "Generates build information file"
    group = "build"
    
    val outputFile = file("$buildDir/generated/buildInfo.properties")
    outputs.file(outputFile)
    
    doLast {
        outputFile.parentFile.mkdirs()
        outputFile.writeText("""
            version=${project.version}
            buildTime=${System.currentTimeMillis()}
            gitCommit=${providers.exec {
                commandLine("git", "rev-parse", "HEAD")
            }.standardOutput.asText.get().trim()}
        """.trimIndent())
    }
}

// Make build depend on our custom task
tasks.processResources {
    dependsOn("generateBuildInfo")
    from("$buildDir/generated")
}

// Configure test task
tasks.test {
    useJUnitPlatform()
    
    testLogging {
        events("passed", "skipped", "failed")
        exceptionFormat = TestExceptionFormat.FULL
        showStandardStreams = false
    }
    
    // Parallel test execution
    maxParallelForks = Runtime.getRuntime().availableProcessors() / 2 + 1
    
    // System properties for tests
    systemProperty("junit.jupiter.execution.parallel.enabled", "true")
    systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
}

// Configure JAR task
tasks.jar {
    manifest {
        attributes(
            "Main-Class" to "com.example.MainKt",
            "Implementation-Title" to project.name,
            "Implementation-Version" to project.version
        )
    }
}

Fat JAR Creation

// build.gradle.kts
plugins {
    kotlin("jvm")
    id("com.github.johnrengelman.shadow") version "8.1.1"
}

// Configure Shadow JAR (Fat JAR)
tasks.shadowJar {
    archiveClassifier.set("")
    manifest {
        attributes["Main-Class"] = "com.example.MainKt"
    }
    
    // Minimize JAR size by removing unused classes
    minimize {
        exclude(dependency("ch.qos.logback:.*"))
    }
    
    // Relocate dependencies to avoid conflicts
    relocate("com.fasterxml.jackson", "shaded.jackson")
}

// Alternative: Fat JAR without plugin
tasks.register("fatJar") {
    description = "Creates a fat JAR with all dependencies"
    group = "build"
    
    archiveClassifier.set("fat")
    
    from(sourceSets.main.get().output)
    dependsOn(configurations.runtimeClasspath)
    from({
        configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
    })
    
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    
    manifest {
        attributes["Main-Class"] = "com.example.MainKt"
    }
}

Kotlin Multiplatform Configuration

Basic Multiplatform Setup

// build.gradle.kts
plugins {
    kotlin("multiplatform") version "1.9.10"
    kotlin("plugin.serialization") version "1.9.10"
}

kotlin {
    jvm {
        compilations.all {
            kotlinOptions.jvmTarget = "17"
        }
        withJava()
    }
    
    js(IR) {
        browser {
            testTask {
                useKarma {
                    useChromeHeadless()
                }
            }
        }
        nodejs()
    }
    
    val hostOs = System.getProperty("os.name")
    val isMingwX64 = hostOs.startsWith("Windows")
    val nativeTarget = when {
        hostOs == "Mac OS X" -> macosX64("native")
        hostOs == "Linux" -> linuxX64("native")
        isMingwX64 -> mingwX64("native")
        else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
    }
    
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1")
            }
        }
        
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        
        val jvmMain by getting {
            dependencies {
                implementation("io.ktor:ktor-server-netty:2.3.4")
                implementation("ch.qos.logback:logback-classic:1.4.11")
            }
        }
        
        val jsMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-html:0.8.1")
            }
        }
        
        val nativeMain by getting {
            dependencies {
                // Native-specific dependencies
            }
        }
    }
}

Advanced Gradle Features

Build Variants and Flavors

// build.gradle.kts
val buildVariants = listOf("development", "staging", "production")

buildVariants.forEach { variant ->
    tasks.register("jar${variant.capitalize()}") {
        description = "Creates JAR for $variant environment"
        group = "build"
        
        archiveClassifier.set(variant)
        from(sourceSets.main.get().output)
        
        manifest {
            attributes["Main-Class"] = "com.example.MainKt"
            attributes["Environment"] = variant
        }
    }
}

// Configuration-specific source sets
sourceSets {
    create("development") {
        java.srcDir("src/development/kotlin")
        resources.srcDir("src/development/resources")
    }
    
    create("production") {
        java.srcDir("src/production/kotlin")
        resources.srcDir("src/production/resources")
    }
}

Publishing Configuration

// build.gradle.kts
plugins {
    kotlin("jvm")
    `maven-publish`
    signing
}

publishing {
    publications {
        create("maven") {
            from(components["java"])
            
            pom {
                name.set(project.name)
                description.set(project.description)
                url.set("https://github.com/example/my-kotlin-lib")
                
                licenses {
                    license {
                        name.set("Apache License 2.0")
                        url.set("https://www.apache.org/licenses/LICENSE-2.0")
                    }
                }
                
                developers {
                    developer {
                        id.set("example")
                        name.set("Example Developer")
                        email.set("[email protected]")
                    }
                }
                
                scm {
                    connection.set("scm:git:git://github.com/example/my-kotlin-lib.git")
                    developerConnection.set("scm:git:ssh://github.com/example/my-kotlin-lib.git")
                    url.set("https://github.com/example/my-kotlin-lib")
                }
            }
        }
    }
    
    repositories {
        maven {
            name = "sonatype"
            url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
            credentials {
                username = project.findProperty("ossrhUsername") as String? ?: ""
                password = project.findProperty("ossrhPassword") as String? ?: ""
            }
        }
    }
}

signing {
    sign(publishing.publications["maven"])
}

Performance Optimization

Gradle Configuration

# gradle.properties
# Enable Gradle daemon
org.gradle.daemon=true

# Increase memory for Gradle daemon
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError

# Enable parallel builds
org.gradle.parallel=true

# Enable configuration cache (Gradle 6.6+)
org.gradle.configuration-cache=true

# Enable build cache
org.gradle.caching=true

# Kotlin compilation optimizations
kotlin.incremental=true
kotlin.incremental.useClasspathSnapshot=true
kotlin.build.report.output=file

# Kotlin compiler daemon
kotlin.daemon.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m

Build Cache Configuration

// build.gradle.kts
buildCache {
    local {
        isEnabled = true
        directory = File(rootDir, "build-cache")
        removeUnusedEntriesAfterDays = 30
    }
    
    remote {
        url = uri("https://example.com/build-cache/")
        isEnabled = true
        isPush = true
        credentials {
            username = "cache-user"
            password = "cache-password"
        }
    }
}

// Make tasks cacheable
tasks.withType {
    outputs.cacheIf { true }
}

Debugging and Troubleshooting

Common Gradle Commands

# Build the project
./gradlew build

# Clean and build
./gradlew clean build

# Run tests
./gradlew test

# Show dependencies
./gradlew dependencies

# Show dependency insight
./gradlew dependencyInsight --dependency kotlin-stdlib

# Build with debug info
./gradlew build --info --debug

# Profile build performance
./gradlew build --profile

# Show build scan
./gradlew build --scan

# Refresh dependencies
./gradlew build --refresh-dependencies

Build Script Debugging

// build.gradle.kts
println("Configuring project: ${project.name}")

tasks.register("debugDependencies") {
    description = "Debug dependency configuration"
    group = "help"
    
    doLast {
        configurations.forEach { config ->
            println("Configuration: ${config.name}")
            config.dependencies.forEach { dep ->
                println("  - ${dep.group}:${dep.name}:${dep.version}")
            }
        }
    }
}

// Log task execution
gradle.taskGraph.whenReady {
    allTasks.forEach { task ->
        task.doFirst {
            println("Executing task: ${task.name}")
        }
    }
}

Best Practices

Build Script Organization

  • Use Kotlin DSL: Prefer .kts over .gradle for type safety
  • Extract common configuration: Use convention plugins for shared logic
  • Version catalogs: Centralize dependency versions
  • Avoid hardcoding: Use project properties and environment variables
  • Incremental builds: Configure tasks for optimal caching

Dependency Management Best Practices

// build.gradle.kts
dependencies {
    // โœ… Use specific versions
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    
    // โŒ Avoid dynamic versions
    // implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:+")
    
    // โœ… Use BOM for consistent versions
    implementation(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
    
    // โœ… Separate API and implementation dependencies
    api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    implementation("ch.qos.logback:logback-classic:1.4.11")
    
    // โœ… Use appropriate scopes
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    runtimeOnly("mysql:mysql-connector-java:8.0.33")
}

Key Takeaways

  • Gradle provides powerful build automation for Kotlin projects
  • Kotlin DSL offers type-safe build script configuration
  • Version catalogs help manage dependencies centrally
  • Multi-module projects enable better code organization
  • Performance optimization requires proper configuration
  • Custom tasks extend build functionality

Practice Exercises

  1. Set up a multi-module Kotlin project with shared dependencies
  2. Create custom Gradle tasks for code generation and deployment
  3. Configure a Kotlin multiplatform project with JVM and JS targets
  4. Implement a publishing pipeline with proper artifact signing

Quiz

  1. What's the difference between implementation and api dependencies?
  2. How do version catalogs help with dependency management?
  3. When should you use a fat JAR vs a regular JAR?
Show Answers
  1. implementation dependencies are internal to the module, while api dependencies are exposed to consumers of the module.
  2. Version catalogs centralize dependency versions, enable type-safe accessors, and make it easier to manage dependencies across multi-module projects.
  3. Use fat JARs for standalone applications that need all dependencies bundled, use regular JARs for libraries or when dependencies are managed externally.