R8, ProGuard & Release Optimization
Shrink, obfuscate, and optimize your APK for production without breaking your app
Open interactive version (quiz + challenge)Real-world analogy
What is it?
R8 is Android's default code shrinker, obfuscator, and optimizer that processes your app's bytecode during release builds. It removes unused code (tree shaking), renames identifiers to short names (obfuscation), applies compiler optimizations, and works alongside resource shrinking to produce the smallest possible APK. ProGuard rule files (-keep rules) tell R8 which code must not be touched, especially code accessed via reflection.
Real-world relevance
In a large e-commerce app, enabling R8 reduced the APK from 28MB to 16MB. But the first release broke Gson deserialization for the entire product catalog — all model classes were obfuscated, so JSON field names no longer matched. The fix required adding @SerializedName to 47 model classes and -keep rules for the API response package. After that, the team migrated to kotlinx.serialization with codegen, which generates serialization code at compile time and works perfectly with R8 — no keep rules needed.
Key points
- ProGuard vs R8 — ProGuard was the original shrinker/obfuscator for Android. R8 replaced it as the default since AGP 3.4+. R8 is faster, produces smaller output, and operates directly on DEX bytecode rather than Java bytecode. R8 is a drop-in replacement — it reads ProGuard rule files but has its own optimizations like class merging and devirtualization.
- Enabling R8 in build.gradle — Set minifyEnabled true and shrinkResources true in your release buildType. R8 runs automatically when minifyEnabled is true. proguardFiles points to your keep rules. Always test the release build thoroughly — minification can break reflection-based code.
- proguard-rules.pro vs consumer-rules.pro — proguard-rules.pro applies to your app module only. consumer-rules.pro is for library modules — rules are automatically included when the library is consumed by the app. Libraries should use consumer-rules.pro to protect their public API from obfuscation.
- -keep rule syntax — -keep class com.example.Model { *; } preserves the class and all its members. -keepclassmembers keeps members but allows class name obfuscation. -keepnames keeps names but allows shrinking unused code. -keepclasseswithmembernames keeps classes that have specific members. The granularity matters for APK size.
- -dontwarn and -dontnote — -dontwarn com.thirdparty.** suppresses warnings for missing classes (common with optional dependencies). -dontnote suppresses informational messages. Use these sparingly — they can mask real problems. Prefer fixing the root cause over silencing warnings.
- Code shrinking (tree shaking) — R8 analyzes the call graph from entry points (Activities, Services, etc.) and removes all unreachable code. This can reduce APK size by 20-50%. Entry points are determined by AndroidManifest.xml entries and -keep rules. Unused library code is also stripped.
- Obfuscation & name mangling — R8 renames classes, methods, and fields to short names (a, b, c) making reverse engineering harder and reducing string pool size. It produces a mapping.txt file that maps obfuscated names back to originals. Always archive mapping.txt with each release for crash report deobfuscation.
- Reflection-based issues (Gson, Retrofit, Room) — Gson uses reflection to map JSON fields to class properties — obfuscation renames fields and breaks deserialization. Solutions: use @SerializedName annotations, add -keep rules for model classes, or switch to Moshi/kotlinx.serialization with codegen. Retrofit needs -keep for API interfaces. Room handles its own rules via consumer-rules.
- Debugging with mapping.txt and retrace — When a crash occurs in a minified build, the stack trace shows obfuscated names. Use the retrace tool (bundled with SDK) or upload mapping.txt to Play Console / Firebase Crashlytics for automatic deobfuscation. Command: retrace mapping.txt stacktrace.txt. Always upload mapping.txt to your crash reporting service.
- Resource shrinking — shrinkResources true removes unused resources (drawables, layouts, strings) that code shrinking made unreachable. It works with minifyEnabled true. Use tools:keep and tools:discard in res/raw/keep.xml for resources loaded dynamically. Can save 10-30% additional size.
- Common crash patterns after enabling R8 — Top issues: (1) Serialization models obfuscated — JSON parsing fails silently or crashes. (2) Enum values stripped — when used via reflection. (3) Kotlin metadata removed — breaks Kotlin reflection. (4) Missing rules for DI frameworks (Dagger/Hilt). (5) WebView JavaScript interface methods obfuscated. Always add -keep for @JavascriptInterface methods.
- R8 full mode vs compat mode — R8 full mode (android.enableR8.fullMode=true, default since AGP 8.0) applies more aggressive optimizations: class merging, more inlining, and assumes -keepattributes is not implicit. It may break libraries that relied on ProGuard compat behavior. Test thoroughly when upgrading.
Code example
// build.gradle.kts (app module)
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// Upload mapping file to Crashlytics
firebaseCrashlytics {
mappingFileUploadEnabled = true
}
}
}
}
// proguard-rules.pro — Common rules for production apps
# Keep all model classes used with Gson
-keep class com.example.app.data.model.** { *; }
# Keep Retrofit API interfaces
-keep,allowobfuscation interface com.example.app.data.api.** {
@retrofit2.http.* <methods>;
}
# Keep classes with @SerializedName annotation
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# Keep enum values (required for serialization)
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Keep Parcelable implementations
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# Keep JavaScript interface methods for WebView
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# Keep Room entities (Room's consumer rules handle most,
# but custom TypeConverters need explicit rules)
-keep class com.example.app.data.db.converters.** { *; }
# Kotlin metadata for reflection (if used)
-keep class kotlin.Metadata { *; }
# Suppress warnings for optional dependencies
-dontwarn org.bouncycastle.**
-dontwarn okhttp3.internal.platform.**
// consumer-rules.pro (for a library module)
// These rules are automatically applied when the library is consumed
-keep public class com.example.library.PublicApi { *; }
-keep public interface com.example.library.Callback { *; }Line-by-line walkthrough
- 1. isMinifyEnabled = true — enables R8 code shrinking and obfuscation for the release build type; without this, your APK contains all code including unused library methods
- 2. isShrinkResources = true — removes unused resources (images, layouts, strings) that became unreachable after code shrinking; requires minifyEnabled
- 3. getDefaultProguardFile('proguard-android-optimize.txt') — includes Google's default rules that keep Activity/Service/etc. referenced from AndroidManifest
- 4. -keep class com.example.app.data.model.** { *; } — preserves all model classes and their fields from obfuscation, critical for Gson reflection-based deserialization
- 5. -keep,allowobfuscation interface ... — keeps Retrofit interfaces but allows their names to be obfuscated; the method annotations are preserved for Retrofit's reflection
- 6. @com.google.gson.annotations.SerializedName — keeps any field annotated with @SerializedName, an alternative to keeping entire model packages
- 7. -keepclassmembers enum * { values(); valueOf(); } — preserves enum methods required for deserialization; without this, enums break when deserialized from JSON or Bundles
- 8. android.os.Parcelable$Creator — preserves the CREATOR field required by the Parcelable contract; without it, unparceling crashes at runtime
- 9. @android.webkit.JavascriptInterface — keeps methods callable from WebView JavaScript; obfuscation would make them unreachable from JS code
- 10. -dontwarn org.bouncycastle.** — suppresses warnings for optional crypto dependencies that may not be on the classpath; only use after verifying they are truly optional
Spot the bug
// build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
// shrinkResources is missing
}
}
}
// Model class — no ProGuard rules defined
data class ApiResponse(
val status: String,
val data: UserProfile
)
data class UserProfile(
val userId: String,
val displayName: String,
val avatarUrl: String?
)
// Retrofit usage
val response = gson.fromJson(json, ApiResponse::class.java)
// response.data.displayName is always null in release builds!Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Shrink, Obfuscate, and Optimize Your App (developer.android.com)
- R8 Full Mode (r8.googlesource.com)
- ProGuard Manual — Keep Rules (guardsquare.com)
- Firebase Crashlytics Deobfuscation (firebase.google.com)
- kotlinx.serialization Guide (kotlinlang.org)