O Gradle é o sistema de build padrão para projetos Kotlin e Android, e quando combinado com o Kotlin DSL (arquivos .kts), oferece autocompletion, verificação de tipos em tempo de compilação e toda a expressividade da linguagem. Neste tutorial, vamos explorar desde a configuração básica do build.gradle.kts até tópicos avançados como projetos multi-módulo, version catalogs e criação de tasks customizadas.
Por que Kotlin DSL no Gradle?
Tradicionalmente, arquivos Gradle usam Groovy, uma linguagem dinâmica sem verificação de tipos. Com Kotlin DSL, você ganha suporte completo do IDE — autocompletion, navegação para definições, refatoração e erros em tempo de compilação. Como o arquivo de build é Kotlin puro, você pode usar funções, classes, condicionais e tudo que a linguagem oferece diretamente na configuração.
Passo 1: Estrutura Básica do build.gradle.kts
Vamos começar com a estrutura fundamental de um projeto Kotlin com Gradle:
// build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
application
}
group = "com.exemplo"
version = "1.0.0"
application {
mainClass.set("com.exemplo.MainKt")
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("io.ktor:ktor-server-core:2.3.12")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.3")
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}
Cada seção tem uma responsabilidade clara: plugins ativa funcionalidades, repositories define de onde baixar dependências, dependencies lista as bibliotecas necessárias, e kotlin configura o compilador.
Passo 2: Gerenciando Dependências
No Gradle, as dependências são organizadas por configurações que definem quando e como elas são usadas:
dependencies {
// Disponível em compile-time e runtime
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Disponível em compile-time, mas não exposta para consumidores
implementation("com.google.code.gson:gson:2.11.0")
// Exposta para consumidores da sua biblioteca
api("org.jetbrains.exposed:exposed-core:0.52.0")
// Apenas em compile-time (ex: processadores de annotation)
compileOnly("org.projectlombok:lombok:1.18.34")
// Apenas em runtime
runtimeOnly("org.postgresql:postgresql:42.7.3")
// Apenas para testes
testImplementation("io.mockk:mockk:1.13.12")
// Plataforma (BOM) para alinhar versões
implementation(platform("io.ktor:ktor-bom:2.3.12"))
implementation("io.ktor:ktor-server-core") // versão vem do BOM
}
A diferença entre implementation e api é crucial em projetos multi-módulo: api expõe a dependência para quem consome seu módulo, enquanto implementation a mantém privada. Sempre prefira implementation para reduzir o classpath transitivo.
Passo 3: Plugins
Plugins estendem as capacidades do Gradle. Há duas formas de aplicá-los:
// build.gradle.kts
plugins {
// Plugin do repositório de plugins do Gradle
kotlin("jvm") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0"
// Plugin por ID
id("com.github.johnrengelman.shadow") version "8.1.1"
// Plugin built-in do Gradle (sem versão)
application
`java-library`
}
Para projetos multi-módulo, defina versões de plugins uma única vez no settings.gradle.kts:
// settings.gradle.kts
pluginManagement {
plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0"
}
}
Depois, nos módulos, aplique sem especificar versão:
// modulo/build.gradle.kts
plugins {
kotlin("jvm")
}
Passo 4: Criando Tasks Customizadas
Tasks são a unidade fundamental de trabalho no Gradle. Você pode criar tasks personalizadas usando Kotlin:
// build.gradle.kts
tasks.register("gerarRelatorio") {
group = "documentacao"
description = "Gera relatório de dependências do projeto"
doLast {
val arquivo = file("$buildDir/relatorio.txt")
arquivo.parentFile.mkdirs()
val dependencias = configurations["runtimeClasspath"]
.resolvedConfiguration
.resolvedArtifacts
.map { "${it.moduleVersion.id}" }
.sorted()
arquivo.writeText(dependencias.joinToString("\n"))
println("Relatório gerado em: ${arquivo.absolutePath}")
}
}
tasks.register<Copy>("copiarConfiguracoes") {
from("src/main/resources/config")
into("$buildDir/config")
include("*.yaml", "*.properties")
filter { linha -> linha.replace("localhost", System.getenv("DB_HOST") ?: "localhost") }
}
Tasks tipadas como Copy, Jar, Exec oferecem APIs específicas. Para criar tasks reutilizáveis com lógica complexa, use classes:
abstract class GerarDocTask : DefaultTask() {
@get:Input
abstract val modulo: Property<String>
@get:OutputDirectory
abstract val saida: DirectoryProperty
@TaskAction
fun executar() {
val dir = saida.get().asFile
dir.mkdirs()
File(dir, "docs.txt").writeText("Documentação do módulo: ${modulo.get()}")
}
}
tasks.register<GerarDocTask>("gerarDoc") {
modulo.set("core")
saida.set(layout.buildDirectory.dir("docs"))
}
Passo 5: Projetos Multi-Módulo
Projetos grandes se beneficiam de uma arquitetura modular. Veja a estrutura típica:
// settings.gradle.kts
rootProject.name = "meu-projeto"
include(":core")
include(":api")
include(":app")
Cada módulo tem seu próprio build.gradle.kts e pode depender de outros:
// api/build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
implementation(project(":core"))
implementation("io.ktor:ktor-server-core:2.3.12")
}
Para compartilhar configurações entre módulos, use subprojects ou allprojects no build raiz:
// build.gradle.kts (raiz)
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
}
}
}
Passo 6: Version Catalogs
O Version Catalog (introduzido no Gradle 7.0) centraliza todas as versões de dependências em um único arquivo TOML:
// gradle/libs.versions.toml
[versions]
kotlin = "2.0.0"
ktor = "2.3.12"
exposed = "0.52.0"
coroutines = "1.8.1"
junit = "5.10.3"
[libraries]
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
[bundles]
ktor-server = ["ktor-server-core", "ktor-server-netty"]
exposed = ["exposed-core", "exposed-dao"]
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
No build.gradle.kts, use as referências tipadas:
dependencies {
implementation(libs.coroutines.core)
implementation(libs.bundles.ktor.server)
implementation(libs.bundles.exposed)
testImplementation(libs.junit.jupiter)
}
O Version Catalog gera acessors type-safe, então o IDE oferece autocompletion completo. Bundles agrupam dependências relacionadas, simplificando a declaração em múltiplos módulos.
Passo 7: buildSrc para Lógica Compartilhada
O diretório buildSrc/ é compilado automaticamente pelo Gradle e disponibilizado em todos os scripts de build. Use-o para convention plugins e constantes:
// buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
// buildSrc/src/main/kotlin/kotlin-conventions.gradle.kts
plugins {
kotlin("jvm")
}
group = "com.exemplo"
repositories {
mavenCentral()
}
kotlin {
jvmToolchain(21)
}
tasks.test {
useJUnitPlatform()
}
Depois, aplique em qualquer módulo:
// core/build.gradle.kts
plugins {
id("kotlin-conventions")
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
O buildSrc é excelente para eliminar duplicação entre módulos. Para projetos muito grandes, considere migrar para Composite Builds com convention plugins em um diretório separado como build-logic/.
Erros Comuns
1. Misturar sintaxe Groovy com Kotlin DSL: Strings com aspas simples ('valor') não existem em Kotlin. Use sempre aspas duplas. Atribuições com = são obrigatórias no Kotlin DSL (Groovy permite omitir).
2. Não usar kotlin("test") para testes: Essa dependência inclui automaticamente as assertions do Kotlin e o adapter correto para a plataforma. Sem ela, você precisa importar assertions do JUnit diretamente.
3. Esquecer useJUnitPlatform(): Sem essa configuração na task test, o Gradle não encontra nem executa os testes JUnit 5. Esse é um dos erros mais frustrantes para iniciantes.
4. Version Catalog com nomes inválidos: No TOML, use hífens nos nomes de bibliotecas. O Gradle converte para pontos nos acessors Kotlin (ex: ktor-server-core vira libs.ktor.server.core).
5. buildSrc recriando cache a cada mudança: Qualquer alteração no buildSrc invalida todo o cache do Gradle. Para projetos grandes, prefira Composite Builds que oferecem invalidação granular.
Conclusão e Próximos Passos
Neste tutorial, exploramos o Gradle com Kotlin DSL de ponta a ponta: desde a estrutura básica do build.gradle.kts até técnicas avançadas como version catalogs, projetos multi-módulo e buildSrc. Dominar o Gradle é essencial para qualquer desenvolvedor Kotlin, pois o sistema de build impacta diretamente a produtividade e manutenibilidade do projeto.
Como próximos passos, estude o Gradle Configuration Cache para builds mais rápidos, explore plugins como Shadow para fat JARs e Detekt para análise estática de código Kotlin. Para aprofundar seu conhecimento em Kotlin DSL, confira nosso tutorial dedicado sobre como criar DSLs em Kotlin.