Instrumentação ou morte: Como domei o Gemini CLI no meu projeto Android
por Nikolay Vlasov
Gerar código tornou-se demasiado fácil.
Tive uma experiência reveladora: num projeto pessoal, experimentei o "vibe-coding" quase sem revisão — seguindo o princípio "o que o agente escrever, aceitamos". No início, parecia rápido e conveniente. Mas, em pouco tempo, o projeto começou a desmoronar: o código tornou-se pesado, de difícil manutenção, parte da funcionalidade quebrava, surgiram atrasos, fugas de memória e, no fundo, todo o conjunto de problemas que costumam acumular-se ao longo de meses — aqui surgiram quase de imediato.
A certa altura, tornou-se óbvio: o problema não é o agente escrever código "mau". O problema é a velocidade com que esse código se acumula sem controlo.
O que falha com esta abordagem
Quando removemos o controlo manual, começam a manifestar-se padrões típicos:
- evasão das camadas arquiteturais;
- duplicação da lógica;
- limites de módulos difusos;
- dependências aleatórias;
- complexificação desnecessária do código;
- degradação gradual do desempenho.
O importante é que tudo isto não acontece num instante. Cada passo individual parece "normal", mas, no conjunto, o sistema sai rapidamente de controlo.
Por que um único prompt não foi suficiente
A minha primeira tentativa foi bastante óbvia — reuni um grande rules.md onde incluí:
- restrições arquiteturais;
- naming conventions;
- regras de camadas;
- acordos locais do projeto.
Isto ajudou parcialmente, mas não resolveu o problema.
Na prática, verificou-se que:
- contextos longos funcionam de forma instável;
- parte das regras acaba por ser ignorada com o tempo;
- o modelo nem sempre aplica as restrições de forma consistente;
- o custo e o tempo de resposta crescem com o tamanho do prompt.
No final, cheguei a uma conclusão mais pragmática: As regras importantes devem ser verificadas, e não apenas descritas.
1. Konsist: a arquitetura como um contrato executável
Por padrão, o agente escolhe o caminho de implementação mais simples. Se puder ignorar uma camada — ele fá-lo-á.
Para limitar isto, comecei a descrever a arquitetura através de testes com o Konsist (uma ferramenta para verificar regras arquiteturais de código Kotlin através de testes unitários).
Exemplo:
@Test
fun `use cases should have UseCase suffix and reside in domain package`() {
val classes = Konsist.scopeFromProject().classes()
val violations = classes
.filter { classDeclaration ->
classDeclaration.name?.endsWith("UseCase") != true ||
!classDeclaration.resideInPackage("com.core.domain")
}
if (violations.isNotEmpty()) {
val message = buildString {
appendLine("VIOLATION: UseCase naming conventions not followed")
appendLine("FIX: Rename class to end with 'UseCase'")
appendLine("FIX: Move class to com.core.domain package")
}
fail(message)
}
}
Aqui, duas coisas são fundamentais para mim:
- A verificação deteta a violação;
- A mensagem fornece uma direção clara sobre como corrigi-la.
O agente deixa de trabalhar "no vácuo" e passa a trabalhar dentro de um ciclo de feedback.
2. Compressão de logs: menos ruído — iterações mais rápidas
Um dos problemas com que me deparei foi o volume dos logs.
Se dermos ao agente o output completo do Gradle ou do JUnit, ele simplesmente perde-se em todo esse volume. O contexto enche-se de ruído em vez de sinal.
Por isso, criei uma camada simples de compressão:
- mantenho apenas os testes que falharam;
- extraio uma mensagem de erro curta;
- limito o stacktrace;
- removo tudo o que é desnecessário.
Exemplo:
def parse_xml_reports(root_dir):
summary = []
for testcase in root.findall(".//testcase"):
failure = testcase.find("failure")
if failure is not None:
message = failure.get("message", "No message")
text = failure.text or ""
stacktrace = "\n".join(text.strip().split("\n")[:15])
summary.append(f"FAILED: {testcase.get('name')}")
summary.append(f"Message: {message}")
summary.append(f"Stacktrace:\n{stacktrace}\n")
return summary
Depois disso, o ciclo "quebrou → reparou" tornou-se visivelmente mais rápido e previsível.
3. Spotless e detekt: higiene básica sem intervenção humana
A camada seguinte é a verificação automática de qualidade:
- Spotless (uma ferramenta universal para formatação automática de código);
- detekt (analisador estático para Kotlin para encontrar problemas potenciais e estruturas complexas).
Deixei de ver isto como "ferramentas adicionais". É simplesmente parte do processo.
Se o código não passar nestas verificações — não é considerado pronto. O próprio agente volta atrás e corrige.
Isto elimina:
- pequenas correções nas revisões;
- discussões sobre o estilo;
- degradação gradual da legibilidade.
4. Traduções: eliminação de erros mecânicos
O ficheiro strings.xml revelou-se um ponto de problemas inesperado.
Os LLMs erram regularmente com:
- apóstrofos;
- aspas;
- sequências de escape.
Não tentei "ensinar" isto ao modelo através de texto. Revelou-se mais simples adicionar:
- uma verificação;
- correção automática;
- trabalho através de parsing XML.
Importante: sem alterações globais bruscas — apenas correções pontuais.
5. Desempenho e tamanho
Um outro efeito que se manifestou ao longo do tempo foi a degradação invisível do produto.
O agente pode resolver a tarefa, mas, ao mesmo tempo:
- adicionar uma dependência pesada;
- complexificar o código;
- aumentar o tamanho da build;
- afetar o desempenho.
Para monitorizar isto, adicionei:
- benchmarks básicos;
- controlo do tamanho da build.
Isto não dá uma garantia total, mas permite, pelo menos, notar desvios a tempo.
6. Pre-commit: filtro rápido (Mercurial/hg ou Git)
Movi parte das verificações para o pre-commit:
- auto-formatação;
- linting pontual;
- verificações arquiteturais para módulos críticos.
A ideia não é "proibir tudo", mas sim:
- detetar rapidamente problemas básicos;
- não os levar para o repositório;
- não sobrecarregar a revisão com ruído desnecessário.
Por que funciona
A essência do desenvolvimento não mudou. Testes, linters e restrições arquiteturais sempre foram a norma.
O que mudou foi outra coisa: a velocidade a que o código aparece.
Quando as alterações são geradas rapidamente e em grandes volumes, o controlo manual deixa de ser escalável. O que antes se conseguia "detetar a olho", agora simplesmente não há tempo para processar.
Neste contexto, as verificações automáticas deixam de ser uma "boa prática" e tornam-se numa necessidade básica — simplesmente para manter o sistema num estado estável.
Conclusão
Não vejo esta abordagem como universal ou obrigatória para todos.
Mas no meu caso, deu um efeito bastante percetível — em primeiro lugar devida à redução da carga cognitiva.
Já não preciso de:
- manter todas as restrições na cabeça;
- rever manualmente cada alteração;
- analisar logs enormes;
- reconfirmar constantemente as bases.
O sistema encarrega-se disso, e eu intervenho onde realmente faz sentido.
Dos benefícios práticos que notei:
- iterações mais estáveis sem regressões aleatórias;
- comportamento do agente previsível através do feedback;
- ciclo de correção mais rápido;
- menos ruído na revisão;
- mais fácil escalar a utilização de agentes.
Isto não resolve todos os problemas, mas torna o processo de trabalho com um agente sensivelmente mais gerível — pelo menos na minha experiência.
Links úteis
- Konsist — documentação atualizada sobre o lint arquitetural para Kotlin.
- Spotless — ferramenta para formatação automática de código.
- detekt — analisador estático de código para Kotlin.
- Mercurial (hg) — sistema de controlo de versões.
- Maestro — plataforma para a automatização de testes de interface (UI) móvel.
No próximo artigo, explicarei como adicionei a isto a execução automática de cenários de interface e a verificação da interface através do MCP Mobile e do Maestro (framework para testes de UI móvel simples e declarativos).