O que e KSP em Kotlin?
KSP (Kotlin Symbol Processing) e uma API desenvolvida pelo Google para processar anotacoes e gerar codigo em tempo de compilacao em projetos Kotlin. Ele e o substituto moderno do kapt (Kotlin Annotation Processing Tool), oferecendo performance ate 2x melhor ao trabalhar diretamente com os simbolos do Kotlin em vez de gerar stubs Java intermediarios.
KSP entende conceitos nativos do Kotlin como propriedades, extension functions, nullable types e data classes, que o kapt (baseado no modelo Java) nao consegue representar adequadamente.
Por que KSP em vez de kapt?
O kapt funciona gerando stubs Java a partir do codigo Kotlin, e depois executando processadores de anotacao Java padrao sobre esses stubs. Isso tem varios problemas:
- Performance: gerar stubs e lento e consome memoria.
- Perda de informacao: conceitos Kotlin (como nullable, data class, sealed class) se perdem na traducao para Java.
- Manutencao: os processadores precisam inferir informacoes Kotlin a partir da representacao Java.
KSP resolve tudo isso acessando a arvore de simbolos Kotlin diretamente.
Configurando KSP no projeto
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.22"
id("com.google.devtools.ksp") version "1.9.22-1.0.17"
}
dependencies {
// Adicionar processadores KSP
ksp("com.example:meu-processador:1.0.0")
}
Para projetos multiplataforma:
plugins {
kotlin("multiplatform") version "1.9.22"
id("com.google.devtools.ksp") version "1.9.22-1.0.17"
}
dependencies {
add("kspJvm", "com.example:meu-processador:1.0.0")
add("kspIosArm64", "com.example:meu-processador:1.0.0")
}
Criando um processador KSP simples
Um processador KSP implementa a interface SymbolProcessor:
// Processador
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
class MeuProcessador(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val simbolos = resolver.getSymbolsWithAnnotation("com.example.AutoFactory")
.filterIsInstance<KSClassDeclaration>()
simbolos.forEach { classe ->
gerarFactory(classe)
}
// Retorna simbolos nao processados para uma proxima rodada
return emptyList()
}
private fun gerarFactory(classe: KSClassDeclaration) {
val nomeClasse = classe.simpleName.asString()
val nomePacote = classe.packageName.asString()
val nomeFactory = "${nomeClasse}Factory"
val arquivo = codeGenerator.createNewFile(
dependencies = Dependencies(true, classe.containingFile!!),
packageName = nomePacote,
fileName = nomeFactory
)
arquivo.writer().use { writer ->
writer.write("""
package $nomePacote
object $nomeFactory {
fun criar(): $nomeClasse {
return $nomeClasse()
}
}
""".trimIndent())
}
logger.info("Factory gerada: $nomeFactory")
}
}
Provider do processador
O processador precisa de um SymbolProcessorProvider:
class MeuProcessadorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return MeuProcessador(
codeGenerator = environment.codeGenerator,
logger = environment.logger
)
}
}
Registre o provider no arquivo resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider:
com.example.MeuProcessadorProvider
Exemplo pratico: gerador de Builder
// Anotacao
annotation class AutoBuilder
// Processador que gera builders
class BuilderProcessador(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
resolver.getSymbolsWithAnnotation("com.example.AutoBuilder")
.filterIsInstance<KSClassDeclaration>()
.forEach { gerarBuilder(it) }
return emptyList()
}
private fun gerarBuilder(classe: KSClassDeclaration) {
val nome = classe.simpleName.asString()
val pacote = classe.packageName.asString()
val propriedades = classe.primaryConstructor?.parameters ?: return
val builder = buildString {
appendLine("package $pacote")
appendLine()
appendLine("class ${nome}Builder {")
propriedades.forEach { param ->
val nomeProp = param.name?.asString() ?: return@forEach
val tipo = param.type.resolve().declaration.qualifiedName?.asString() ?: "Any"
appendLine(" var $nomeProp: $tipo? = null")
}
appendLine()
appendLine(" fun build(): $nome {")
val args = propriedades.joinToString(", ") { param ->
val n = param.name?.asString() ?: ""
"$n = $n ?: throw IllegalStateException(\"$n nao definido\")"
}
appendLine(" return $nome($args)")
appendLine(" }")
appendLine("}")
}
codeGenerator.createNewFile(
Dependencies(true, classe.containingFile!!),
pacote, "${nome}Builder"
).writer().use { it.write(builder) }
}
}
Uso:
@AutoBuilder
data class Pessoa(val nome: String, val idade: Int)
// Codigo gerado automaticamente:
// PessoaBuilder com metodos nome(), idade() e build()
fun main() {
val pessoa = PessoaBuilder().apply {
nome = "Ana"
idade = 30
}.build()
}
Navegando a arvore de simbolos
O KSP fornece uma API rica para inspecionar o codigo Kotlin:
fun inspecionarClasse(classe: KSClassDeclaration) {
// Nome e pacote
val nome = classe.simpleName.asString()
val pacote = classe.packageName.asString()
// Modificadores
val eDataClass = Modifier.DATA in classe.modifiers
val eSealed = Modifier.SEALED in classe.modifiers
// Propriedades
classe.getAllProperties().forEach { prop ->
val nomeProp = prop.simpleName.asString()
val tipo = prop.type.resolve()
val nullable = tipo.isMarkedNullable
}
// Funcoes
classe.getAllFunctions().forEach { func ->
val nomeFunc = func.simpleName.asString()
val parametros = func.parameters
}
// Supertipos
classe.superTypes.forEach { superTipo ->
val tipoResolvido = superTipo.resolve()
}
}
Quando usar KSP
- Reducao de boilerplate: gerar builders, factories, mappers e adaptadores automaticamente.
- Validacao em tempo de compilacao: verificar que anotacoes estao sendo usadas corretamente.
- Frameworks e bibliotecas: Room, Moshi, Koin e outras bibliotecas usam KSP para geracao de codigo.
- Serializacao customizada: gerar serializadores e desserializadores especificos.
- Documentacao automatica: extrair informacoes do codigo para gerar documentacao.
Erros comuns
Versao incompativel do KSP com Kotlin: a versao do KSP deve corresponder exatamente a versao do Kotlin (ex: KSP 1.9.22-1.0.17 para Kotlin 1.9.22).
Nao declarar dependencies corretamente: o CodeGenerator precisa saber de quais arquivos o codigo gerado depende para invalidacao correta do cache.
Processar na rodada errada: se um simbolo depende de codigo gerado por outro processador, ele pode nao estar disponivel na primeira rodada. Retorne simbolos nao processados da funcao
process.Ignorar tipos nullable: diferente do kapt, KSP expoe nullability nativamente. Nao tratear isso gera codigo incorreto.
Nao testar o processador: KSP oferece uma API de teste (
kotlin-compile-testing) para verificar que o codigo gerado esta correto.
Termos relacionados
- Gradle Plugin: o plugin KSP e aplicado no build.gradle.kts para habilitar o processamento.
- Annotation: anotacoes que marcam classes e funcoes para processamento pelo KSP.
- Serialization: a biblioteca kotlinx.serialization usa um plugin de compilador similar para gerar codigo.
- kapt: o predecessor do KSP, baseado em annotation processing do Java.
- Code Generation: o resultado principal do KSP e gerar arquivos fonte que sao compilados junto com o projeto.
- Kotlin Multiplatform: KSP suporta projetos multiplataforma, processando codigo de cada target.
KSP e uma ferramenta poderosa para eliminar boilerplate e garantir seguranca em tempo de compilacao. Se voce desenvolve bibliotecas ou frameworks em Kotlin, dominar KSP permite criar APIs que sao ao mesmo tempo expressivas e eficientes.