Qué hace R8 exactamente
R8 (el compilador que reemplazó a ProGuard desde Android Gradle Plugin 3.4) hace tres cosas sobre tu código en el build de release. Entender cada una por separado es clave para saber cuándo algo falla y por qué.
Shrinking — eliminar código muerto
R8 analiza tu app desde sus puntos de entrada (el Manifest, las Activities, los Fragments) y traza un grafo de todo el código alcanzable. Todo lo que no es alcanzable — clases, métodos, campos — se elimina del APK/AAB final.
En una app típica con Jetpack, esto elimina literalmente cientos de miles de métodos que las librerías incluyen pero tu app nunca llama. El tamaño puede reducirse un 30-60%.
Ofuscación — renombrar a nombres sin significado
Después del shrinking, R8 renombra clases, métodos y campos a nombres cortos: a, b, c... El código funciona exactamente igual, pero alguien que haga reverse engineering del APK ve algo ilegible.
Optimization — simplificar código
R8 aplica optimizaciones que van más allá de lo que hacía ProGuard: inlinea métodos cortos, elimina ramas muertas (if (false)), simplifica comparaciones, y más. En apps con Kotlin, donde hay muchas funciones inline y extension functions, esto puede reducir el bytecode generado significativamente.
// Lo que vos escribís:
fun esPar(n: Int) = n % 2 == 0
val numeros = listOf(1, 2, 3, 4)
val pares = numeros.filter { esPar(it) }
// Lo que R8 puede generar (simplificado):
// La función esPar() se inlinea directamente en el filter
// El lambda se elimina y se reemplaza por código directo
// El resultado final es más eficiente que el código original
ProGuard vs R8 — la confusión de nombres
ProGuard es la herramienta original, desarrollada independientemente de Android. R8 es el reemplazo de Google, integrado en el Android Gradle Plugin. Desde AGP 3.4 (2019), R8 es el default.
La confusión persiste porque R8 usa exactamente el mismo formato de reglas que ProGuard. Los archivos se llaman proguard-rules.pro, las reglas empiezan con -keep, y toda la documentación vieja de ProGuard sigue siendo válida. R8 es retrocompatible — es un reemplazo transparente.
Cuando alguien dice "configurar ProGuard" en 2025, en realidad está configurando R8. Los dos nombres se usan indistintamente.
Cómo habilitarlo
// build.gradle (app) — configuración mínima correcta
android {
buildTypes {
release {
// Habilita shrinking + ofuscación + optimization
isMinifyEnabled = true
// Habilita eliminar recursos (imágenes, layouts, strings) no usados
// Solo funciona si minifyEnabled = true
isShrinkResources = true
proguardFiles(
// Reglas base de Google — optimizaciones agresivas
// La diferencia entre proguard-android.txt y proguard-android-optimize.txt:
// el primero deshabilita algunas optimizaciones para mayor estabilidad
// el segundo las habilita todas — es el recomendado con R8
getDefaultProguardFile("proguard-android-optimize.txt"),
// Tus reglas propias
"proguard-rules.pro"
)
}
debug {
// En debug NUNCA minificar — el build sería lento y el debugging difícil
isMinifyEnabled = false
}
}
}
Testear R8 sin esperar al releasePodés crear un build type releaseDebugable que tenga minifyEnabled = true pero también debuggable = true. Así podés adjuntar el debugger y usar el profiler sobre el código minificado, sin tener que subir a Play Store para encontrar un bug.
Qué le pasa a tu código — en concreto
Para entender por qué las reglas son necesarias, hay que entender cuándo R8 falla silenciosamente. Los casos problemáticos son siempre los mismos: cuando el código accede a clases o campos por nombre, en runtime.
// Caso 1: Reflection — R8 no puede trazar qué se usa
val clase = Class.forName("ar.pensa.miapp.data.remote.ProductoDto")
val campo = clase.getDeclaredField("productName")
// Si R8 eliminó o renombró ProductoDto.productName, esto crashea en runtime
// Caso 2: Gson / Moshi — deserialización por nombre de campo
data class ProductoDto(
val productName: String, // Gson busca este campo por nombre en el JSON
val unitPrice: Double
)
// R8 renombra productName → a, unitPrice → b
// Gson no encuentra los campos y retorna null/0 silenciosamente
// Caso 3: Enums accedidos por nombre
val tema = Tema.valueOf("OSCURO") // busca el enum por nombre en runtime
// Si R8 renombró OSCURO → a, esto lanza IllegalArgumentException
// Caso 4: Annotations leídas en runtime
@Retention(AnnotationRetention.RUNTIME)
annotation class MiAnnotation
// Si R8 elimina la annotation, el código que la lee en runtime falla
El peor tipo de bug de R8Cuando Gson no puede deserializar un campo, no lanza una excepción — simplemente lo deja en null o en el valor default. La app "funciona" pero muestra datos vacíos o incorrectos. Estos bugs son difíciles de detectar en QA y pueden llegar a producción.
Entender las reglas — la sintaxis
Las reglas de ProGuard/R8 son instrucciones sobre qué NO tocar. Si no hay regla, R8 es libre de eliminar u ofuscar. Las reglas agregan excepciones.
# Estructura general de una regla:
-keep [opciones] class [especificación de clase] {
[especificación de miembros]
}
# Especificación de clase:
ar.pensa.miapp.data.remote.ProductoDto # clase exacta
ar.pensa.miapp.data.remote.** # paquete y subpaquetes
ar.pensa.miapp.data.remote.* # solo el paquete (sin subpaquetes)
* extends ar.pensa.miapp.domain.model.BaseModel # todo lo que extiende BaseModel
* implements java.io.Serializable # todo lo que implementa Serializable
# Especificación de miembros:
*; # todos los campos y métodos
; # todos los campos
; # todos los métodos
(...); # todos los constructores
void procesar(...); # método específico por nombre y tipo
-keep y sus variantes — cuál usar cuándo
Hay varias variantes de -keep y elegir la incorrecta puede dejar demasiado código sin ofuscar (más grande el APK) o eliminar algo que necesitabas.
# -keep — mantiene la clase Y todos sus miembros tal como están
# Usar cuando necesitás acceder a la clase por nombre en runtime
-keep class ar.pensa.miapp.data.remote.ProductoDto { *; }
# -keepnames — mantiene los nombres pero permite eliminar miembros no usados
# Usar cuando necesitás el nombre de la clase pero no todos sus miembros
-keepnames class ar.pensa.miapp.MiException
# -keepclassmembers — mantiene los miembros especificados pero permite
# que la clase sea renombrada o eliminada si no es alcanzable
# Usar para campos que se acceden por reflection (Gson, Parcelable, etc.)
-keepclassmembers class ar.pensa.miapp.data.remote.** {
;
}
# -keepclasseswithmembers — mantiene la clase Y sus miembros
# SI la clase contiene esos miembros
# Muy útil para Parcelable:
-keepclasseswithmembers class * {
public static final android.os.Parcelable$Creator *;
}
# -dontwarn — suprimir warnings sobre clases que no existen en runtime
# (librerías que referencian cosas opcionales)
-dontwarn okhttp3.**
-dontwarn kotlin.reflect.**
Las reglas de las librerías — no las escribas a mano
Esta es la parte que más gente no sabe: las librerías bien mantenidas incluyen sus propias reglas de ProGuard dentro del AAR. Cuando Gradle las incorpora, sus reglas se aplican automáticamente. No tenés que escribir reglas para Retrofit, Room, Hilt, Gson, OkHttp, ni para la mayoría de las librerías populares.
# Ver TODAS las reglas aplicadas (las tuyas + las de cada librería):
# Después de un build de release, revisar:
app/build/outputs/mapping/release/configuration.txt
# Este archivo contiene el merge de todas las reglas.
# Si algo falla, acá podés ver si la regla que necesitabas está incluida
# o si falta porque la librería no la incluyó.
Si una librería no incluye sus reglas (las viejas o mal mantenidas), buscalas en su README o en su issue tracker. Nunca uses -keep class ** { *; } para "solucionar" un problema — eso deshabilita toda la ofuscación de tu app.
El caso más común: Gson, Retrofit y serialización
Este es el escenario que más breaks en producción genera. Si usás Gson (directamente o a través de Retrofit con GsonConverterFactory), tus DTOs deben estar protegidos:
# proguard-rules.pro — reglas para Gson
# Los DTOs se deserializan por nombre de campo — R8 no puede renombrarlos
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName ;
}
# Si usás @SerializedName en TODOS los campos, eso es suficiente.
# Si no usás @SerializedName, necesitás mantener TODO el paquete de DTOs:
-keep class ar.pensa.miapp.data.remote.dto.** { *; }
// La solución más robusta: usar @SerializedName en todos los campos
// Así R8 sabe qué nombres son críticos y puede ofuscar el resto
data class ProductoDto(
@SerializedName("product_name") val nombre: String,
@SerializedName("unit_price") val precio: Double,
@SerializedName("stock_count") val stock: Int
)
// Con @SerializedName no necesitás reglas keep para los campos
// R8 sabe que esos strings son referencias de runtime
Kotlinx.serialization no tiene este problemaA diferencia de Gson, kotlinx.serialization genera código de serialización en tiempo de compilación — no usa reflection en runtime. R8 puede ofuscar los DTOs completamente. Si empezás un proyecto nuevo, es una razón más para preferir kotlinx.serialization sobre Gson.
Debuggear cuando algo se rompe con R8
Los síntomas de un problema de R8 son siempre similares: la app funciona en debug pero falla en release. El proceso de diagnóstico:
# Paso 1: reproducir en un build "release debuggable"
# Crear un build type en build.gradle:
create("releaseDebug") {
initWith(getByName("release"))
isDebuggable = true
isMinifyEnabled = true
signingConfig = signingConfigs.getByName("debug")
}
# Paso 2: identificar si el problema es shrinking u ofuscación
# Primero deshabilitar solo la ofuscación:
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
# En proguard-rules.pro, agregar temporalmente:
-dontobfuscate # deshabilita solo la ofuscación, mantiene shrinking
# Si el problema desaparece → es un problema de ofuscación (nombre de clase/campo)
# Si el problema persiste → es un problema de shrinking (algo fue eliminado)
# Paso 3: para problemas de shrinking, buscar en usage.txt:
app/build/outputs/mapping/release/usage.txt
# Este archivo lista TODO lo que fue eliminado
# grep por el nombre de la clase que buscás
// Síntoma típico de problema de shrinking:
// ClassNotFoundException, NoSuchMethodException, NoSuchFieldException
// Síntoma típico de problema de ofuscación:
// NullPointerException donde no debería haber null
// (Gson deserializó pero no encontró el campo → null)
// IllegalArgumentException en Enum.valueOf()
// ClassCastException inesperado
Desobfuscar crashes de producción
Cuando llega un crash de producción, el stack trace viene ofuscado. El mapping.txt permite traducirlo de vuelta al código original:
# El mapping.txt se genera en cada build de release:
app/build/outputs/mapping/release/mapping.txt
# Desobfuscar con retrace (incluido en las Android Command Line Tools):
retrace mapping.txt crash.txt
# O usar el retrace online de Android Studio:
# Build → Analyze APK → seleccionar el APK
# O directamente en el stack trace: click derecho → "Deobfuscate stack trace"
# Si subís a Play Store, Google guarda el mapping automáticamente.
# Crashlytics lo usa para mostrar stack traces desobfuscados en la consola
# sin que tengas que hacer nada manual.
# IMPORTANTE: guardá el mapping.txt de cada versión publicada
# Sin el mapping.txt de la versión exacta que crasheó, no podés desobfuscar
# Nombralos con la versión: mapping-1.2.3.txt
Verificar el resultado — ¿cuánto ahorró?
Después de cada build de release, vale la pena verificar el impacto de R8:
# APK Analyzer en Android Studio:
# Build → Analyze APK → seleccionar app-release.apk
# Muestra el tamaño de cada componente (DEX, recursos, assets)
# y dentro del DEX, el tamaño de cada paquete
# Desde línea de comandos con apkanalyzer:
# Tamaño del APK:
apkanalyzer apk file-size app-release.apk
apkanalyzer apk download-size app-release.apk # tamaño de descarga comprimido
# Cuántas clases/métodos quedan después del shrinking:
apkanalyzer dex references app-release.apk
# Límite de DEX: 65536 métodos. Si superás este número, necesitás multidex.
# Comparar antes/después:
apkanalyzer apk compare app-debug.apk app-release.apk
Un número útil para evaluar qué tan bien funciona R8: el ratio entre el tamaño del debug APK y el release APK. En proyectos con Jetpack + muchas librerías, un ratio de 3:1 (release es 3 veces más pequeño) es razonable. Ratios menores pueden indicar que las reglas keep están siendo demasiado amplias.
Las reglas mínimas recomendadas para empezar
Este es un proguard-rules.pro razonable como punto de partida para la mayoría de los proyectos:
# proguard-rules.pro — punto de partida
# ── KOTLIN ───────────────────────────────────────────────────
# Mantener metadata de Kotlin para reflection y serialización
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses
-keepattributes EnclosingMethod
# ── ENUMS ────────────────────────────────────────────────────
# Necesario si accedés a enums por nombre (Enum.valueOf, SharedPreferences, Room)
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# ── PARCELABLE ────────────────────────────────────────────────
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# ── SERIALIZABLE ─────────────────────────────────────────────
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# ── MODELOS DE DATOS (ajustar según la app) ───────────────────
# Si usás Gson SIN @SerializedName en todos los campos:
# -keep class ar.pensa.miapp.data.remote.dto.** { *; }
# Si usás @SerializedName: solo esto es suficiente
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName ;
}
# ── SUPRIMIR WARNINGS DE LIBRERÍAS ───────────────────────────
-dontwarn javax.annotation.**
-dontwarn kotlin.reflect.jvm.internal.**