Resposta rápida: em um app Android moderno, R8 é a ferramenta executada no build de release para remover código e recursos não usados, otimizar bytecode e renomear classes, métodos e campos. Ative isMinifyEnabled = true e isShrinkResources = true, comece com o arquivo otimizado padrão do Android e adicione regras -keep somente quando uma biblioteca ou um mecanismo dinâmico realmente precisar delas. Teste o artefato de release, preserve o mapping.txt de cada versão e investigue warnings em vez de copiar um arquivo ProGuard gigantesco da internet. Kotlin, Compose e coroutines normalmente não exigem regras genéricas; os problemas aparecem principalmente com reflexão, serialização baseada em nomes, JNI, WebView JavaScript, carregamento por string e bibliotecas mal configuradas.

É comum um aplicativo funcionar perfeitamente em debug e quebrar apenas depois de gerar o APK ou AAB de produção. Uma tela deixa de abrir, a desserialização devolve objetos vazios, uma classe “desaparece”, o stack trace chega ofuscado ou o build falha com Missing classes detected while running R8. Quase sempre, a diferença está no processo de shrinking e otimização aplicado à variante de release.

Este guia explica R8 e as chamadas “regras ProGuard” sem transformar o arquivo de configuração em uma coleção de superstições. Você verá como habilitar o processo, diferenciar os arquivos envolvidos, proteger apenas APIs dinâmicas, diagnosticar remoções indevidas, testar o release e manter regras próprias em projetos modularizados. Para melhorar performance além do tamanho do pacote, combine este trabalho com Baseline Profiles e Macrobenchmark e com o guia de performance Kotlin.

R8 e ProGuard são a mesma coisa?

Não exatamente. ProGuard é uma ferramenta histórica de shrinking e ofuscação para bytecode Java. R8 é a implementação usada pelo Android Gradle Plugin no fluxo Android atual. R8 entende o ecossistema Android, integra shrinking, otimização, ofuscação e conversão para DEX e aceita a linguagem de configuração popularizada pelo ProGuard.

Por isso, os projetos ainda têm arquivos chamados proguard-rules.pro, e a documentação de bibliotecas costuma publicar “ProGuard rules”, mesmo quando quem executa as regras é o R8.

Na prática, pense assim:

TermoPapel no projeto Android
R8ferramenta que analisa, otimiza, remove e renomeia código no build
Regras ProGuardlinguagem de configuração aceita pelo R8
proguard-android-optimize.txtregras padrão otimizadas fornecidas pelo Android Gradle Plugin
proguard-rules.proregras específicas do módulo ou aplicativo
consumer-rules.proregras que uma library Android entrega ao aplicativo consumidor
mapping.txtmapa entre nomes originais e nomes ofuscados da versão

O objetivo principal não é “esconder o código”. A ofuscação aumenta o custo de leitura do artefato, mas não torna segredos seguros. Chaves privadas, tokens administrativos e credenciais de backend nunca devem estar no app. O valor mais consistente do R8 é reduzir o artefato e otimizar o código, mantendo somente o que pode ser alcançado em runtime.

O que o R8 faz no build de release

O R8 combina quatro operações relacionadas:

  1. Shrinking: remove classes, métodos e campos considerados inalcançáveis.
  2. Otimização: simplifica fluxos, propaga valores, remove branches e pode fazer inline.
  3. Ofuscação: troca nomes longos por nomes curtos.
  4. Adaptação do artefato: produz bytecode apropriado para o fluxo DEX do Android.

Quando isShrinkResources também está ativo, o Android Gradle Plugin pode remover recursos que ficaram sem referência após o shrinking de código. A combinação importa: uma Activity eliminada pode tornar seu layout removível; uma feature realmente usada deve continuar alcançável pelo manifesto, por chamadas estáticas ou por regras que descrevam o acesso dinâmico.

O analisador trabalha muito bem quando o grafo de chamadas é explícito. Ele enxerga uma função chamando outra, uma classe listada no manifesto e boa parte das referências geradas pelo compilador. Ele não consegue adivinhar com a mesma segurança uma classe construída a partir de texto:

val className = remoteConfig.getString("payment_handler_class")
val handlerClass = Class.forName(className)
val handler = handlerClass.getDeclaredConstructor().newInstance()

Para o R8, não existe uma referência estática entre esse código e a implementação carregada. Esse é o tipo de caso em que uma regra pode ser necessária — ou, preferencialmente, em que a arquitetura pode trocar nomes de classe por um registro explícito e seguro.

Como ativar minificação e remoção de recursos

No módulo app, configure a variante de release no Gradle Kotlin DSL:

android {
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            isShrinkResources = true

            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro",
            )
        }
    }
}

Use o arquivo otimizado padrão, salvo quando a documentação da versão do Android Gradle Plugin adotada pelo time orientar algo diferente. Evite copiar o conteúdo desse arquivo para dentro do repositório: a versão fornecida pelo plugin evolui junto com o toolchain.

O proguard-rules.pro pode começar pequeno:

# Regras específicas do aplicativo.
# Não mantenha pacotes inteiros sem uma razão verificável.

Depois, gere e instale o release localmente:

./gradlew :app:assembleRelease
./gradlew :app:bundleRelease

Em um projeto com assinatura separada de produção, também pode existir uma variante de benchmark ou staging minificada. Ela é útil para testar comportamento próximo do release sem distribuir a chave de produção no ambiente de desenvolvimento.

Se a configuração Gradle ainda está espalhada pelos módulos, revise Gradle Version Catalog com Kotlin e o guia de Kotlin com Gradle.

Por que o debug funciona e o release quebra?

A variante debug normalmente mantém nomes e código não usado, desabilita várias otimizações e inclui ferramentas de inspeção. O release minificado muda essas condições. As falhas mais comuns são:

  • biblioteca acessando classe por reflexão sem regras adequadas;
  • serializador dependendo do nome do campo depois da ofuscação;
  • framework esperando annotation ou assinatura genérica removida;
  • código nativo procurando método Java/Kotlin pelo nome;
  • método exposto ao JavaScript de uma WebView não preservado;
  • implementação criada por ServiceLoader ou nome textual;
  • regra ampla de otimização expondo um bug de concorrência ou inicialização;
  • recurso removido porque a referência é montada dinamicamente.

A primeira resposta não deve ser -keep class ** { *; }. Essa regra praticamente desliga o benefício do R8 e pode esconder a causa. Reproduza a falha no release, identifique qual símbolo precisa continuar acessível e escreva a menor regra possível.

Entendendo -keep, -keepclassmembers e -keepnames

As regras parecem parecidas, mas expressam contratos diferentes.

Preservar a classe inteira

-keep class br.com.exemplo.pagamento.PixHandler { *; }

A classe e seus membros são mantidos e não renomeados. É uma regra forte. Use somente se o runtime realmente depende de toda a superfície.

Preservar membros específicos

-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

A regra protege métodos anotados que serão chamados pelo JavaScript. Ela não precisa preservar todos os métodos de todas as classes.

Preservar nomes

-keepnames class br.com.exemplo.analytics.EventPayload

O símbolo pode continuar sujeito a outras transformações permitidas, mas seu nome é preservado. Isso pode atender integrações que dependem do nome sem exigir um -keep completo. Confirme, porém, se a classe também precisa ser instanciada ou acessada dinamicamente; apenas manter o nome não cria alcançabilidade.

Usar annotations como contrato

Uma annotation própria deixa a intenção próxima do código:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class KeepForReflection

@KeepForReflection
data class PaymentPlugin(
    val id: String,
)
-keep @interface br.com.exemplo.KeepForReflection
-keep @br.com.exemplo.KeepForReflection class * { *; }

Outra opção é androidx.annotation.Keep, especialmente para casos pontuais. Para libraries reutilizáveis, regras de consumidor normalmente oferecem uma separação melhor que obrigar cada app a conhecer detalhes internos.

Kotlin e Jetpack Compose precisam de regras especiais?

Em geral, não adicione regras globais para Kotlin, coroutines ou Compose. O toolchain atual e as bibliotecas oficiais são projetados para trabalhar com R8. Uma regra como esta é um sinal de alerta:

# Evite: ampla demais.
-keep class kotlin.** { *; }
-keep class kotlinx.coroutines.** { *; }
-keep class androidx.compose.** { *; }

Ela aumenta o pacote, reduz otimizações e não explica qual integração dinâmica estaria quebrando.

Compose gera bastante código, mas chamadas composable e referências normais continuam visíveis no grafo. O que pode precisar de atenção não é “ser Compose”, e sim usar reflexão, preview tooling dentro do artefato errado, navegação baseada em nomes ou uma library externa com regras incompletas.

Também não preserve todas as data class por padrão. Modelos Kotlin usados diretamente por código compilado podem ser renomeados sem problema. O risco aparece quando um serializador ou protocolo depende de nomes JVM em runtime.

Para investigar tamanho e estabilidade de UI, veja o configurador de estabilidade do Compose e o guia de modularização Android com Kotlin e Compose.

Serialização: quando regras são necessárias

A resposta depende da biblioteca e de como ela encontra os modelos.

kotlinx.serialization

kotlinx.serialization usa código gerado pelo plugin do compilador. Isso reduz a dependência de reflexão típica de serializadores antigos. Ainda assim, siga a documentação da versão usada, principalmente se houver serialização polimórfica, classes carregadas dinamicamente ou módulos separados.

Declare nomes de protocolo explicitamente quando eles fazem parte do contrato externo:

@Serializable
data class UserResponse(
    @SerialName("user_id")
    val userId: String,
    @SerialName("display_name")
    val displayName: String,
)

Assim, o JSON não depende do nome Kotlin/JVM do campo.

Moshi com codegen

Moshi com codegen também transforma a maior parte do trabalho em referências geradas e previsíveis:

@JsonClass(generateAdapter = true)
data class UserResponse(
    @Json(name = "user_id") val userId: String,
)

Prefira codegen a reflexão quando o projeto já usa KSP. O guia de KSP no Kotlin explica a configuração e a migração do kapt.

Gson e reflexão

Gson frequentemente trabalha por reflexão sobre campos. Regras e annotations podem ser necessárias conforme os modelos e a versão usada. Em vez de preservar um pacote de domínio inteiro, marque nomes do payload com @SerializedName e siga as recomendações oficiais da biblioteca.

data class UserResponse(
    @SerializedName("user_id")
    val userId: String,
)

Uma regra baseada em annotation pode ser mais estreita que -keep class br.com.exemplo.model.** { *; }. O ponto essencial é validar o comportamento no release com payloads reais, incluindo campos opcionais, listas vazias e respostas de erro.

Reflection, DI, Room e outras libraries

Bibliotecas maduras normalmente publicam suas próprias regras de consumidor. Room, por exemplo, usa geração de código; Hilt também gera componentes. Não cole regras antigas só porque uma resposta de fórum de 2019 as recomendou.

Ao integrar uma library:

  1. leia a documentação oficial sobre R8/ProGuard;
  2. verifique se o AAR já contém regras de consumidor;
  3. faça um build minificado com warnings tratados;
  4. teste os fluxos que usam geração de código ou reflexão;
  5. adicione regra no app apenas se o contrato do app exigir.

Para dependency injection, compare os modelos no guia Koin vs Hilt com Kotlin e Android. Se uma biblioteca obriga o app a manter pacotes internos inteiros, considere isso um custo técnico e acompanhe atualizações que reduzam a necessidade.

consumer-rules.pro em libraries Android

Se você desenvolve uma library, a library deve carregar as regras necessárias ao próprio funcionamento. Não transfira detalhes internos para cada aplicativo consumidor.

No módulo de library:

android {
    defaultConfig {
        consumerProguardFiles("consumer-rules.pro")
    }
}

Exemplo de regra para uma API realmente criada por reflexão:

-keep class br.com.exemplo.sdk.internal.PluginRegistry { *; }
-keepclassmembers,allowoptimization class * {
    @br.com.exemplo.sdk.PublicCallback <methods>;
}

Regras de consumidor acompanham o AAR e entram no shrinking do aplicativo final. Elas devem ser mínimas: uma library que publica -keep class ** { *; } prejudica todos os clientes.

Diferencie os arquivos:

  • proguard-rules.pro: otimiza ou preserva código quando o próprio módulo é processado;
  • consumer-rules.pro: instrui o processo do aplicativo consumidor sobre requisitos da library.

Em projetos modulares internos, essa separação também é valiosa. O módulo dono da reflexão documenta o requisito, em vez de concentrar todas as regras no :app.

Como investigar Missing classes detected while running R8

Um erro típico de build mostra classes ausentes e pode gerar um arquivo com sugestões. Não aplique automaticamente todas as sugestões sem entender a origem.

Siga esta ordem:

  1. encontre qual dependência referencia a classe ausente;
  2. confirme se a dependência opcional realmente deveria estar presente;
  3. atualize a library se a versão tiver metadados ou regras defeituosas;
  4. adicione a dependência correta quando a classe for usada em runtime;
  5. use -dontwarn somente quando a referência for opcional e comprovadamente inacessível no seu app.

Exemplo de decisão ruim:

-dontwarn **

Isso silencia sinais importantes no projeto inteiro. Uma regra estreita, após investigação, é menos arriscada:

# Somente se o módulo opcional não é usado por este aplicativo.
-dontwarn com.vendor.optional.**

Mesmo assim, documente o motivo e a versão da dependência. Um futuro upgrade pode mudar a alcançabilidade e transformar o warning ignorado em crash real.

Ferramentas do Gradle ajudam a entender a árvore:

./gradlew :app:dependencies
./gradlew :app:dependencyInsight \
  --dependency nome-da-library \
  --configuration releaseRuntimeClasspath

Descobrindo por que uma classe foi mantida ou removida

R8 oferece regras de diagnóstico úteis durante investigação. Elas não precisam ficar permanentemente no arquivo principal.

Para entender por que algo foi mantido:

-whyareyoukeeping class br.com.exemplo.legacy.LegacyParser

Para verificar se uma regra casa com classes ou membros esperados, use ferramentas e relatórios oferecidos pela versão do R8/Android Gradle Plugin adotada pelo projeto. Os nomes e caminhos dos outputs podem variar; confira o diretório de mapping da variante e a documentação da versão.

Arquivos comuns de interesse incluem:

  • mapping.txt: mapa para desfazer a ofuscação de stack traces;
  • seeds.txt: símbolos preservados por regras, quando gerado;
  • usage.txt: itens removidos, quando gerado;
  • configuration.txt: configuração combinada aplicada ao build, quando disponível.

Esses relatórios respondem perguntas melhores que tentativa e erro. Se um pacote inteiro aparece em seeds.txt, procure uma regra ampla de uma dependência ou do próprio app. Se uma classe esperada aparece em usage.txt, descubra como ela é acessada em runtime e descreva esse acesso de forma mínima.

Mapping, Retrace e stack traces de produção

Depois da ofuscação, um stack trace pode chegar assim:

java.lang.IllegalStateException
    at a.b.c(Unknown Source:17)
    at d.e.f(Unknown Source:42)

Sem o mapping.txt exato daquela versão, a investigação perde contexto. Cada release precisa guardar seu mapping como artefato do CI e enviá-lo aos serviços de observabilidade usados pelo time.

O Retrace converte o stack trace ofuscado para nomes originais usando esse mapa. O fluxo conceitual é:

retrace path/da-versao/mapping.txt stacktrace.txt

Use a ferramenta distribuída com o toolchain compatível com seu build. Não reutilize o mapping da versão seguinte, mesmo que apenas uma linha tenha mudado: a associação de nomes pode ser diferente.

Se o app usa Firebase Crashlytics, configure o plugin para enviar os símbolos e mappings de cada variante distribuída. Depois, confirme no painel se uma falha de teste aparece com stack trace legível. O artigo de Firebase Crashlytics, ANRs e logs com Kotlin mostra como fechar esse ciclo de observabilidade.

Testando um build minificado de verdade

Um build verde não prova que o app funciona. O teste precisa executar o artefato com R8 ativo.

Uma estratégia prática tem três camadas:

1. Smoke test manual

Instale o release ou uma variante minificada e percorra:

  • inicialização limpa;
  • login e logout;
  • navegação principal;
  • leitura e escrita no banco;
  • desserialização de respostas;
  • notificações e deep links;
  • upload, câmera ou seletor de arquivos;
  • feature flags e telas pouco acessadas;
  • fluxo que chama SDKs externos.

2. Testes instrumentados na variante correta

Crie uma variante stagingRelease ou equivalente, com backend controlado e minificação ativa. Rode testes de UI contra ela em device ou emulator representativo.

buildTypes {
    create("benchmark") {
        initWith(getByName("release"))
        signingConfig = signingConfigs.getByName("debug")
        matchingFallbacks += listOf("release")
        isDebuggable = false
    }
}

A configuração exata depende dos plugins do projeto. O princípio é ter um artefato reproduzível, minificado e seguro para automação.

3. Rollout gradual e observabilidade

Mesmo com testes, faça rollout progressivo quando a loja e o processo do time permitirem. Compare taxa de crash, ANR, tempo de startup e falhas por versão. Não atribua automaticamente toda regressão ao R8: mudanças de backend, migrations, fabricantes e recursos novos também podem ser a causa.

Para automatizar o pipeline, consulte o guia de CI/CD para Kotlin e o artigo sobre GitHub Actions em projetos Kotlin.

R8 reduz o tamanho do APK e do AAB quanto?

Não existe uma porcentagem universal. O resultado depende de:

  • quantidade de código não usado nas dependências;
  • número e tamanho de recursos;
  • uso de reflection;
  • regras -keep amplas;
  • arquitetura modular e feature delivery;
  • bibliotecas nativas, que podem dominar o tamanho;
  • idiomas, densidades e formatos de imagem;
  • recursos já filtrados pelo App Bundle.

Meça seu próprio artefato. Compare variantes equivalentes e use o APK Analyzer do Android Studio para localizar os maiores pacotes, recursos e arquivos nativos. Um ganho modesto ainda pode ser valioso se vier junto de otimizações, mas tamanho não deve justificar regras agressivas sem testes.

Também evite comparar diretamente o tamanho do .aab enviado com o download final do usuário. O App Bundle é uma unidade de publicação; a Play Store gera APKs apropriados ao dispositivo. Compare métricas equivalentes e acompanhe a estimativa de download fornecida pelas ferramentas de distribuição.

R8 é uma medida de segurança?

R8 ajuda a dificultar engenharia reversa casual porque renomeia símbolos e remove estruturas não usadas. Isso é defesa em profundidade, não uma fronteira de segurança.

Um atacante ainda pode:

  • extrair strings e recursos;
  • observar tráfego em um dispositivo comprometido;
  • instrumentar métodos em runtime;
  • analisar bibliotecas nativas;
  • encontrar endpoints e fluxos do aplicativo;
  • reconstruir parte da lógica com tempo suficiente.

Portanto:

  • nunca coloque segredo de servidor no app;
  • valide autorização no backend;
  • use comunicação segura e rotação de tokens;
  • proteja dados locais conforme o risco;
  • trate Play Integrity e mecanismos semelhantes como sinais, não prova absoluta;
  • não use ofuscação para esconder prática insegura.

Veja também segurança de dados locais no Android e LGPD em apps Android com Kotlin.

Erros comuns com R8 e regras ProGuard

Manter todos os modelos

-keep class br.com.exemplo.model.** { *; }

Essa regra pode funcionar, mas costuma ser ampla demais. Separe modelos de rede, banco e domínio; use adapters gerados e annotations de protocolo; preserve apenas o que depende de reflexão.

Copiar dezenas de regras sem saber a origem

Arquivos acumulados sobrevivem a migrations e upgrades. A library passa a oferecer consumer rules, mas o app continua mantendo tudo. Revise cada bloco com comentário, dependência e motivo.

Usar -dontoptimize como solução permanente

Desabilitar otimização pode ajudar a isolar um problema, mas não deveria ser a primeira solução definitiva. Reduza o caso, atualize o toolchain e encontre a transformação ou o código incorreto.

Testar apenas debug

É a causa clássica de crash tardio. Pelo menos os fluxos críticos precisam rodar em uma variante minificada antes da publicação.

Perder o mapping.txt

Sem mapping por versão, stack traces ofuscados podem ficar praticamente inúteis. Armazene o arquivo com identificação de version code, version name e commit.

Tratar warning como ruído

Warnings podem indicar dependência opcional, versão incompatível ou referência quebrada. Silencie somente depois de provar que o caminho não é executado.

Preservar nomes por causa de JSON sem contrato explícito

O formato externo deve usar @SerialName, @Json ou @SerializedName, não depender acidentalmente do nome da propriedade Kotlin.

Checklist para colocar R8 em produção

Use esta sequência para uma adoção segura:

  • ativar isMinifyEnabled na variante de release;
  • ativar isShrinkResources quando aplicável;
  • usar o arquivo padrão otimizado do Android Gradle Plugin;
  • remover regras globais de Kotlin, Compose e coroutines sem justificativa;
  • revisar reflection, JNI, WebView JavaScript e carregamento por string;
  • preferir serialização e DI com codegen;
  • confirmar consumer rules das bibliotecas;
  • resolver ou justificar cada warning de classes ausentes;
  • instalar e testar um artefato minificado;
  • rodar testes instrumentados nos fluxos críticos;
  • guardar e enviar o mapping.txt da versão;
  • validar stack trace desofuscado no serviço de crashes;
  • comparar tamanho e performance com métricas equivalentes;
  • fazer rollout gradual e monitorar crash/ANR.

Perguntas frequentes

Preciso escrever regras ProGuard para todo app Kotlin?

Não. Muitos apps funcionam com as regras padrão e as consumer rules das dependências. Regras próprias são necessárias quando o app possui acessos dinâmicos que o R8 não consegue inferir ou quando uma biblioteca não descreve corretamente seus requisitos.

R8 pode remover uma Activity ou Service declarada no manifesto?

Componentes corretamente referenciados pelo manifesto fazem parte das raízes consideradas pelo toolchain. Problemas tendem a surgir com componentes carregados por nome fora dos mecanismos esperados, manifests transformados incorretamente ou integrações dinâmicas próprias. Sempre confira o manifesto mesclado da variante.

@Keep resolve qualquer crash de release?

Não. @Keep é útil para preservar um símbolo específico, mas pode apenas mascarar uma regra mal definida. Entenda se o problema é nome, membro, annotation, assinatura, construtor ou alcançabilidade antes de aplicar.

Posso usar R8 sem ofuscar nomes?

É possível configurar atributos como -dontobfuscate para diagnóstico ou necessidades específicas, mantendo shrinking e parte das otimizações. Porém, use isso conscientemente e teste o resultado; não é necessário desabilitar ofuscação apenas porque um stack trace ficou ilegível — o caminho correto é preservar e usar o mapping.

R8 melhora tempo de startup?

Pode reduzir código e aplicar otimizações, mas o efeito varia. Startup também depende de inicialização de bibliotecas, I/O, banco, rede, composição da UI e compilação no dispositivo. Meça com Macrobenchmark e considere AndroidX App Startup e Baseline Profiles para tratar o problema completo.

Devo adicionar -keepattributes *?

Quase nunca. Preservar todos os atributos aumenta o artefato e reduz a precisão do shrinking. Mantenha somente os atributos exigidos pelas bibliotecas ou mecanismos usados, seguindo a documentação correspondente.

Conclusão

R8 funciona melhor quando o código expressa dependências de forma explícita e cada acesso dinâmico possui um contrato claro. Ative a minificação cedo, teste uma variante próxima do release e mantenha o arquivo de regras pequeno, comentado e verificável. Se uma correção exige preservar o projeto inteiro, você ainda não encontrou a causa.

O fluxo saudável é contínuo: novas bibliotecas entram, versões mudam, regras antigas ficam desnecessárias e features criam novos pontos de reflexão. Trate shrinking como parte da engenharia de release, ao lado de testes, observabilidade e performance — não como um comando executado pela primeira vez minutos antes de publicar o app.

Fontes oficiais para continuar