O que é KSP em Kotlin?
KSP (Kotlin Symbol Processing) é uma API desenvolvida pelo Google para processar anotações e gerar código em tempo de compilação em projetos Kotlin. Ele é o substituto moderno do kapt (Kotlin Annotation Processing Tool), oferecendo performance até 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) não consegue representar adequadamente.
Por que KSP em vez de kapt?
O kapt funciona gerando stubs Java a partir do código Kotlin, e depois executando processadores de anotacao Java padrão sobre esses stubs. Isso tem vários problemas:
- Performance: gerar stubs e lento e consome memória.
- Perda de informação: conceitos Kotlin (como nullable, data class, sealed class) se perdem na traducao para Java.
- manutenção: os processadores precisam inferir informações 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 prático: 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)
// Código 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 código 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
}
// Funções
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.
- Validação em tempo de compilação: verificar que anotações estao sendo usadas corretamente.
- Frameworks e bibliotecas: Room, Moshi, Koin e outras bibliotecas usam KSP para geracao de código.
- serialização customizada: gerar serializadores e desserializadores específicos.
- Documentação automatica: extrair informações do código para gerar documentação.
Casos de Uso no Mundo Real
Room Database no Android: A biblioteca Room usa KSP para gerar implementacoes de DAOs (Data Access Objects) e validar queries SQL em tempo de compilação. Ao anotar uma interface com
@Dao, o KSP gera automaticamente todo o código de acesso ao banco de dados SQLite, eliminando boilerplate e garantindo que queries invalidas sejam detectadas antes da execução.Moshi e serialização JSON: O Moshi utiliza KSP para gerar adaptadores JSON eficientes a partir de data classes Kotlin. Em vez de usar reflexao em tempo de execução (lento e propenso a erros), o KSP analisa as propriedades da classe e gera código otimizado de serialização e desserializacao durante a compilação.
Koin e injecao de dependências: O framework Koin utiliza KSP para verificar o grafo de dependências em tempo de compilação, detectando dependências circulares ou faltantes antes mesmo da aplicação ser executada.
Geracao de código para APIs internas: Equipes que mantém SDKs internos usam KSP para gerar automaticamente classes de mapeamento entre modelos de API e modelos de dominio, garantindo que mudancas no schema da API sejam refletidas em erros de compilação em vez de falhas em producao.
Boas Praticas
- Sempre mantenha a versão do KSP sincronizada com a versão do Kotlin. O formato de versionamento do KSP segue o padrão
<versão-kotlin>-<versão-ksp>(ex:1.9.22-1.0.17). - Declare corretamente as dependências no
CodeGeneratorusando o parametroDependencies, indicando quais arquivos fonte originaram o código gerado. Isso garante que o cache incremental funcione corretamente. - Retorne simbolos não processados da função
process()quando eles dependem de código gerado por outros processadores, permitindo que sejam processados em rodadas subsequentes. - Use a API de teste
kotlin-compile-testingpara validar que seu processador gera código correto e que os erros de compilação esperados sao emitidos com mensagens claras. - Prefira KSP a kapt em novos projetos. Se já usa kapt, migre gradualmente verificando se as bibliotecas que você utiliza já oferecem suporte a KSP.
Perguntas Frequentes
P: Posso usar KSP e kapt no mesmo projeto? R: Sim, ambos podem coexistir no mesmo modulo. Isso e útil durante a migração gradual de kapt para KSP. Porem, manter ambos significa que o projeto ainda incorre no custo de performance do kapt para os processadores que ainda não foram migrados.
P: KSP funciona com Kotlin Multiplatform?
R: Sim. O KSP suporta projetos Kotlin Multiplatform. Voce pode configurar processadores para targets específicos usando notacoes como kspJvm, kspIosArm64, etc. no bloco de dependências do Gradle.
P: Qual a diferenca entre KSP e um plugin de compilador Kotlin? R: KSP opera no nivel de simbolos (classes, funções, propriedades) e só pode gerar novos arquivos, não modificar código existente. Um plugin de compilador tem acesso ao IR (Intermediate Representation) e pode transformar o código existente. KSP e mais simples de usar e mais estavel entre versões do Kotlin, enquanto plugins de compilador sao mais poderosos mas exigem manutenção constante.
P: Como depurar um processador KSP?
R: Voce pode usar logger.warn() ou logger.error() para emitir mensagens durante a compilação. Para depuração interativa, execute o build Gradle com a flag --no-daemon -Dorg.gradle.debug=true e conecte um debugger remoto na porta indicada.
Erros comuns
Versão incompativel do KSP com Kotlin: a versão do KSP deve corresponder exatamente a versão 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 código gerado depende para invalidacao correta do cache.
Processar na rodada errada: se um simbolo depende de código gerado por outro processador, ele pode não estar disponivel na primeira rodada. Retorne simbolos não processados da função
process.Ignorar tipos nullable: diferente do kapt, KSP expõe nullability nativamente. Nao tratear isso gera código incorreto.
Nao testar o processador: KSP oferece uma API de teste (
kotlin-compile-testing) para verificar que o código gerado esta correto.
Termos relacionados
- Gradle Plugin: o plugin KSP e aplicado no build.gradle.kts para habilitar o processamento.
- Annotation: anotações que marcam classes e funções para processamento pelo KSP.
- Serialization: a biblioteca kotlinx.serialization usa um plugin de compilador similar para gerar código.
- kapt: o predecessor do KSP, baseado em annotation processing do Java.
- Code Generation: o resultado principal do KSP e gerar arquivos fonte que são compilados junto com o projeto.
- Kotlin Multiplatform: KSP suporta projetos multiplataforma, processando código de cada target.
KSP e uma ferramenta poderosa para eliminar boilerplate e garantir segurança em tempo de compilação. Se você desenvolve bibliotecas ou frameworks em Kotlin, dominar KSP permite criar APIs que são ao mesmo tempo expressivas e eficientes.