Se o build do seu projeto Kotlin demora mais do que deveria, há uma boa chance de que o kapt (Kotlin Annotation Processing Tool) seja o vilão. A boa notícia é que o KSP — Kotlin Symbol Processing — chegou para substituí-lo com ganhos reais de performance e uma API muito mais amigável. Neste guia, vamos explorar tudo sobre KSP: o que é, como configurar, como migrar do kapt e até como criar seu próprio processador.
O que é KSP?
KSP é uma API desenvolvida pelo Google para processar símbolos Kotlin em tempo de compilação. Diferente do kapt, que precisa gerar stubs Java para funcionar, o KSP trabalha diretamente com a árvore de símbolos do compilador Kotlin. Isso significa:
- Builds até 2x mais rápidos — sem a etapa de geração de stubs Java
- API nativa em Kotlin — trabalha com tipos Kotlin diretamente (nullable, extension functions, etc.)
- Multiplatform ready — funciona com Kotlin Multiplatform, diferente do kapt que é JVM-only
- Incrementalidade — KSP suporta processamento incremental out-of-the-box
KSP vs kapt: Comparação de Performance
A diferença de performance é significativa. Em benchmarks do próprio Google com projetos reais:
| Métrica | kapt | KSP | Melhoria |
|---|---|---|---|
| Build limpo | 120s | 65s | ~45% mais rápido |
| Build incremental | 40s | 18s | ~55% mais rápido |
| Uso de memória | Alto | Moderado | ~30% menos |
O motivo principal é simples: kapt precisa rodar o compilador Java (javac) sobre stubs gerados, enquanto KSP pula essa etapa completamente. Se você usa Gradle no seu projeto Kotlin, a migração para KSP é uma das otimizações com melhor custo-benefício.
Configurando KSP no Projeto
Passo 1: Adicionar o plugin KSP
No seu build.gradle.kts do projeto raiz:
plugins {
id("com.google.devtools.ksp") version "2.1.10-1.0.29" apply false
}
Passo 2: Aplicar no módulo
No build.gradle.kts do módulo:
plugins {
id("org.jetbrains.kotlin.jvm")
id("com.google.devtools.ksp")
}
dependencies {
// Exemplo com Room
implementation("androidx.room:room-runtime:2.7.0")
ksp("androidx.room:room-compiler:2.7.0")
// Exemplo com Moshi
implementation("com.squareup.moshi:moshi:1.15.2")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
Note que onde antes usávamos kapt(...), agora usamos ksp(...). A mudança na configuração é mínima.
Passo 3: Configurar argumentos do KSP (opcional)
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.generateKotlin", "true")
}
Migrando do kapt para KSP
A migração é geralmente simples, mas exige atenção. Aqui está o checklist:
1. Verifique se suas bibliotecas suportam KSP:
| Biblioteca | Suporte KSP | Desde |
|---|---|---|
| Room | ✅ | 2.4.0 |
| Moshi | ✅ | 1.13.0 |
| Hilt/Dagger | ✅ | 2.48 |
| Glide | ✅ | 4.14.0 |
| Koin Annotations | ✅ | 1.1.0 |
| MapStruct | ⚠️ Parcial | — |
2. Substitua as dependências:
// Antes (kapt)
plugins {
kotlin("kapt")
}
dependencies {
kapt("androidx.room:room-compiler:2.7.0")
}
// Depois (KSP)
plugins {
id("com.google.devtools.ksp")
}
dependencies {
ksp("androidx.room:room-compiler:2.7.0")
}
3. Remova o plugin kapt se nenhuma outra dependência precisar dele:
// Remova esta linha do plugins {}
kotlin("kapt")
4. Limpe e rebuild:
./gradlew clean build
Se você está migrando um projeto Spring Boot com Kotlin, atenção: o Dagger/Hilt já suporta KSP, mas verifique a compatibilidade com o Spring annotation processing.
Bibliotecas Populares com Suporte KSP
Room (Android)
Room foi uma das primeiras bibliotecas a adotar KSP. Com a flag room.generateKotlin = true, o código gerado é Kotlin puro:
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
@Entity
data class User(
@PrimaryKey val id: Int,
val name: String,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM user")
suspend fun getAll(): List<User>
@Insert
suspend fun insert(user: User)
}
Moshi (Serialização JSON)
Moshi com KSP gera adaptadores JSON sem reflection:
@JsonClass(generateAdapter = true)
data class ApiResponse(
@Json(name = "status_code") val statusCode: Int,
val message: String,
val data: List<Item>
)
Criando um Processador KSP Simples
Vamos criar um processador que gera automaticamente um método toMap() para data classes anotadas. Isso é útil para entender como KSP funciona por dentro — e é o tipo de conhecimento que impressiona em entrevistas técnicas de Kotlin.
Passo 1: Definir a anotação
// annotations/src/main/kotlin/ToMap.kt
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class ToMap
Passo 2: Criar o processador
// processor/src/main/kotlin/ToMapProcessor.kt
class ToMapProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver
.getSymbolsWithAnnotation("com.example.ToMap")
.filterIsInstance<KSClassDeclaration>()
symbols.forEach { classDeclaration ->
if (classDeclaration.classKind != ClassKind.CLASS) {
logger.error("@ToMap só pode ser aplicado em classes", classDeclaration)
return@forEach
}
generateToMap(classDeclaration)
}
return emptyList()
}
private fun generateToMap(classDecl: KSClassDeclaration) {
val className = classDecl.simpleName.asString()
val packageName = classDecl.packageName.asString()
val properties = classDecl.getAllProperties()
val file = codeGenerator.createNewFile(
Dependencies(true, classDecl.containingFile!!),
packageName,
"${className}Extensions"
)
file.bufferedWriter().use { writer ->
writer.write("package $packageName\n\n")
writer.write("fun $className.toMap(): Map<String, Any?> = mapOf(\n")
properties.forEach { prop ->
val propName = prop.simpleName.asString()
writer.write(" \"$propName\" to this.$propName,\n")
}
writer.write(")\n")
}
}
}
class ToMapProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return ToMapProcessor(environment.codeGenerator, environment.logger)
}
}
Passo 3: Usar a anotação
@ToMap
data class Product(
val id: Long,
val name: String,
val price: Double
)
// Código gerado automaticamente pelo KSP:
// fun Product.toMap(): Map<String, Any?> = mapOf(
// "id" to this.id,
// "name" to this.name,
// "price" to this.price,
// )
fun main() {
val product = Product(1, "Kotlin in Action", 49.90)
println(product.toMap())
// {id=1, name=Kotlin in Action, price=49.9}
}
Essa abordagem com extension functions geradas por KSP é um padrão poderoso que combina bem com DSLs em Kotlin.
Dicas e Boas Práticas
- Sempre prefira KSP ao kapt em projetos novos — não há razão para usar kapt em 2026
- Use processamento incremental — KSP suporta nativamente, mas certifique-se de declarar as dependências corretas no
Dependencies(...) - Teste seus processadores — use a biblioteca
com.google.devtools.ksp:symbol-processing-apipara testes unitários - Evite misturar kapt e KSP no mesmo módulo — funciona, mas perde parte dos ganhos de performance
- Monitore os tempos de build — use
./gradlew --scanpara verificar se a migração realmente melhorou
Quando Ainda Usar kapt?
Existem raros cenários onde o kapt ainda é necessário:
- Bibliotecas Java-only que não migraram para KSP (cada vez mais raro)
- Processadores que dependem de APIs específicas do
javax.annotation.processing - Projetos legados em fase de migração gradual
Mas a tendência é clara: o kapt está em modo de manutenção e o KSP é o futuro. Se você está construindo APIs com Ktor ou projetos Kotlin Multiplatform, KSP é a escolha natural.
Conclusão
KSP não é apenas uma melhoria incremental sobre o kapt — é uma mudança de paradigma no processamento de anotações em Kotlin. Com builds mais rápidos, uma API Kotlin-native e suporte a multiplatform, não há motivo para não migrar. Se você quer mergulhar mais fundo no ecossistema Kotlin, explore nossos guias de design patterns e testes com JUnit5 e MockK.
Para quem vem de outras linguagens, vale comparar a abordagem de geração de código do Kotlin com as macros procedurais do Rust ou os geradores de código em Go com go generate. Já em Python, metaclasses e decorators cumprem um papel semelhante ao KSP para geração dinâmica de código.