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
- Set up a multi-module Kotlin project with shared dependencies
- Create custom Gradle tasks for code generation and deployment
- Configure a Kotlin multiplatform project with JVM and JS targets
- Implement a publishing pipeline with proper artifact signing
Quiz
- What's the difference between implementation and api dependencies?
- How do version catalogs help with dependency management?
- When should you use a fat JAR vs a regular JAR?
Show Answers
- implementation dependencies are internal to the module, while api dependencies are exposed to consumers of the module.
- Version catalogs centralize dependency versions, enable type-safe accessors, and make it easier to manage dependencies across multi-module projects.
- Use fat JARs for standalone applications that need all dependencies bundled, use regular JARs for libraries or when dependencies are managed externally.