Qualidade de código em Kotlin não depende só de code review. Review humano é essencial para arquitetura, clareza de regra de negócio e decisões de produto, mas não deveria gastar energia discutindo import fora de ordem, função longa, MagicNumber, complexidade ciclomática ou formatação inconsistente. Para isso existem ferramentas automáticas.
Em 2026, a combinação mais prática para projetos Kotlin continua sendo ktlint para estilo e formatação, detekt para análise estática e um pipeline de CI/CD que bloqueia regressões antes do merge. Esse trio funciona para Android, backend com Ktor, Spring Boot, bibliotecas JVM e projetos Kotlin Multiplatform.
A diferença entre um setup útil e um setup irritante está na calibragem. Se o time ativa todas as regras de uma vez em um projeto legado, a ferramenta vira ruído. Se deixa tudo permissivo demais, vira decoração no build.gradle.kts. O caminho bom é começar com regras que pegam problemas reais, automatizar o que pode ser corrigido sozinho e evoluir a rigidez aos poucos.
ktlint, detekt e testes resolvem problemas diferentes
Antes de configurar plugins, vale separar responsabilidades.
ktlint cuida de formatação e estilo. Ele aplica convenções próximas do estilo oficial do Kotlin: indentação, espaços, quebras de linha, imports e organização visual. É a ferramenta que evita discussões improdutivas sobre como o código “deveria parecer”.
detekt faz análise estática. Ele procura code smells, complexidade excessiva, nomes ruins, blocos grandes demais, exceções engolidas, duplicação conceitual e padrões que costumam dificultar manutenção. Ele não substitui testes, mas ajuda a identificar código que tende a quebrar ou ficar caro de modificar.
Testes verificam comportamento. Um teste com JUnit 5 e MockK responde se uma regra funciona; o guia completo de testes em Kotlin ajuda a decidir onde entram coroutines, Flow, integração e Android; detekt responde se o formato do código está saudável; ktlint responde se o estilo está consistente. Quando as três camadas trabalham juntas, o review fica mais estratégico.
Configurando ktlint com Gradle
O jeito mais comum de usar ktlint em projetos Kotlin é via plugin Gradle. Em um projeto JVM ou Android, adicione o plugin no build.gradle.kts raiz:
plugins {
id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
}
ktlint {
version.set("1.3.1")
android.set(false)
outputToConsole.set(true)
coloredOutput.set(true)
ignoreFailures.set(false)
}
Em projetos Android, ajuste android.set(true) para habilitar algumas convenções específicas. Depois, rode:
./gradlew ktlintCheck
./gradlew ktlintFormat
O ktlintCheck deve rodar no CI e falhar quando houver problema. O ktlintFormat pode rodar localmente antes do commit para corrigir o que é automático. Uma prática simples é documentar no README: “antes de abrir PR, rode ./gradlew ktlintFormat test”.
Para times maiores, também vale integrar com pre-commit hook. Mas cuidado: hook local ajuda, não substitui CI. Desenvolvedor pode pular hook; pipeline não deveria pular qualidade.
Configurando detekt sem travar o time
O detekt também entra pelo Gradle:
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.7"
}
detekt {
buildUponDefaultConfig = true
allRules = false
config.setFrom(files("config/detekt/detekt.yml"))
baseline = file("config/detekt/baseline.xml")
}
dependencies {
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.7")
}
Crie a configuração inicial:
./gradlew detektGenerateConfig
mkdir -p config/detekt
mv config/detekt/detekt.yml config/detekt/detekt.yml
Se o projeto já tem muito código, gere um baseline:
./gradlew detektBaseline
O baseline registra problemas existentes para que o CI bloqueie apenas problemas novos. Isso é importante em sistemas reais. Em vez de tentar “arrumar tudo” em uma semana, o time impede deterioração e agenda limpezas graduais.
Regras que valem ativar cedo
Nem toda regra do detekt tem o mesmo valor. Algumas pegam problemas práticos rapidamente:
LongMethod: funções grandes demais, difíceis de testar.LongParameterList: construtores e métodos com dependência demais.ComplexMethod: lógica condicional excessiva.TooGenericExceptionCaught:catch (Exception)escondendo falhas importantes.SwallowedException: exceção capturada e ignorada.MagicNumber: números sem nome prejudicando intenção do código.NestedBlockDepth: muitosif,whene loops aninhados.ForbiddenComment:TODO,FIXMEe marcadores que não deveriam chegar em produção.
Um trecho problemático que detekt apontaria:
fun calcularTaxa(cliente: Cliente, pedido: Pedido): Double {
var taxa = 0.0
if (cliente.ativo) {
if (pedido.valor > 1000) {
if (cliente.plano == "premium") {
taxa = 0.02
} else {
taxa = 0.05
}
} else {
taxa = 0.08
}
}
return taxa
}
O código funciona, mas a intenção está enterrada em condicionais. Uma versão mais idiomática separa regras:
fun calcularTaxa(cliente: Cliente, pedido: Pedido): Double = when {
!cliente.ativo -> 0.0
cliente.plano == Plano.PREMIUM && pedido.valor > LIMITE_PEDIDO_ALTO -> TAXA_PREMIUM
pedido.valor > LIMITE_PEDIDO_ALTO -> TAXA_PEDIDO_ALTO
else -> TAXA_PADRAO
}
private const val LIMITE_PEDIDO_ALTO = 1_000.0
private const val TAXA_PREMIUM = 0.02
private const val TAXA_PEDIDO_ALTO = 0.05
private const val TAXA_PADRAO = 0.08
Além de satisfazer regras, a segunda versão é mais fácil de revisar, testar e modificar.
Pipeline de CI para GitHub Actions
Em um projeto Kotlin, o pipeline mínimo deve compilar, rodar testes, checar formatação e executar análise estática. Um exemplo com GitHub Actions:
name: Kotlin Quality
on:
pull_request:
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- name: Check formatting
run: ./gradlew ktlintCheck
- name: Static analysis
run: ./gradlew detekt
- name: Tests
run: ./gradlew test
Se o projeto usa Android, substitua ou complemente com tasks como testDebugUnitTest, lintDebug e build variants relevantes. Para backend, inclua testes de integração quando fizer sentido. O guia de CI/CD para Kotlin aprofunda pipelines com GitHub Actions, GitLab CI e Jenkins.
Uma otimização útil é publicar relatórios como artefatos quando algo falha:
- uses: actions/upload-artifact@v4
if: failure()
with:
name: quality-reports
path: |
build/reports/
**/build/reports/
Assim, quem abriu o PR consegue baixar HTML de testes, relatório do detekt e saída de lint sem reproduzir tudo localmente.
Como aplicar em projeto legado
O erro clássico em projeto legado é transformar a primeira execução em uma lista de 900 problemas. Ninguém vai corrigir isso com qualidade no meio de uma sprint. O melhor plano é incremental:
- Adicione ktlint e rode
ktlintFormatem um commit isolado. - Adicione detekt com baseline para problemas existentes.
- Bloqueie novos problemas no CI.
- Escolha uma regra por semana para endurecer.
- Remova partes do baseline conforme módulos forem refatorados.
Separar commits importa. Um commit só de formatação é chato, mas fácil de revisar. Misturar formatação com mudança de regra de negócio cria diff barulhento e aumenta risco.
Para Android, comece por módulos novos ou mais ativos. Para backend, comece por camadas de domínio e serviços com mudança frequente. O objetivo não é “zerar relatório” por vaidade; é reduzir custo de manutenção onde o time mais mexe.
Convenções de time que evitam briga
Ferramenta boa precisa de acordo social. Algumas decisões devem estar explícitas:
- Qual versão de Kotlin, Gradle, detekt e ktlint o projeto usa.
- Se
ktlintFormatpode alterar código automaticamente antes do commit. - Quais regras do detekt são bloqueantes e quais são apenas aviso.
- Como pedir exceção para uma regra em caso específico.
- Quem revisa mudanças no
detekt.yml.
Use supressões com parcimônia:
@Suppress("LongMethod")
fun migracaoTemporaria() {
// código transitório, com issue vinculada para remoção
}
A supressão deve explicar o motivo. Se o time começa a espalhar @Suppress sem justificativa, a ferramenta perdeu autoridade.
Relação com arquitetura e performance
Detekt e ktlint não decidem arquitetura. Eles não sabem se você deveria usar Ktor ou Spring Boot, se vale migrar para coroutines avançadas ou se um módulo deveria virar microsserviço. Mas eles deixam sinais objetivos: classes grandes, acoplamento alto, funções instáveis e complexidade concentrada.
Esses sinais ajudam o time a priorizar refatorações. Se uma classe aparece em todo relatório, muda toda semana e concentra bugs, ela é candidata a extração, testes melhores ou redesenho. Em backends de alta concorrência, o mesmo raciocínio vale para serviços assíncronos com Flow e mensageria com Kafka e RabbitMQ.
Também existe aprendizado fora do ecossistema Kotlin. Times que trabalham com Go costumam automatizar gofmt, go vet e testes desde cedo; vale comparar essa disciplina com os guias de Go para backend no Brasil. A tecnologia muda, mas a ideia é a mesma: tirar decisões repetitivas do review humano.
Checklist recomendado para 2026
Para um projeto Kotlin novo, eu começaria assim:
- Kotlin e JDK atualizados conforme suporte do projeto.
- Gradle com Kotlin DSL.
- ktlint rodando localmente e no CI.
- detekt com configuração versionada em
config/detekt/detekt.yml. - Testes unitários obrigatórios em PR.
- Relatórios publicados como artefatos quando o pipeline falhar.
- Baseline apenas se o projeto já for legado.
- Revisão mensal das regras que geram mais ruído.
Para um projeto existente, não tente virar “perfeito” em uma sexta-feira. Automatize primeiro, bloqueie regressão depois e só então endureça o padrão. Qualidade sustentável é mais parecida com higiene diária do que com mutirão heroico.
Conclusão
Detekt e ktlint são ferramentas pequenas perto das decisões de produto, mas mudam a rotina de um time Kotlin. Elas reduzem ruído no review, tornam padrões explícitos, ajudam pessoas novas a entender o estilo do projeto e criam uma base confiável para evoluir código sem medo.
O segredo é usar automação como apoio, não como burocracia. ktlint deve corrigir o que é mecânico. detekt deve apontar riscos reais. O CI deve proteger a branch principal. E o time deve reservar energia humana para o que ferramenta nenhuma entende bem: clareza de domínio, experiência do usuário, trade-offs de arquitetura e entrega de valor.