Lesson 77 of 83 advanced

R8, ProGuard & Release Optimization

Shrink, obfuscate, and optimize your APK for production without breaking your app

Open interactive version (quiz + challenge)

Real-world analogy

R8 is like a professional editor for a novel. It removes chapters nobody reads (dead code), renames characters to single letters to save space (obfuscation), rewrites wordy sentences to be concise (optimization), and strips out unused illustrations (resource shrinking). But if someone is looking up characters by name in an index (reflection), the editor needs a 'do not rename' sticky note (-keep rules).

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

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. 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. 2. isShrinkResources = true — removes unused resources (images, layouts, strings) that became unreachable after code shrinking; requires minifyEnabled
  3. 3. getDefaultProguardFile('proguard-android-optimize.txt') — includes Google's default rules that keep Activity/Service/etc. referenced from AndroidManifest
  4. 4. -keep class com.example.app.data.model.** { *; } — preserves all model classes and their fields from obfuscation, critical for Gson reflection-based deserialization
  5. 5. -keep,allowobfuscation interface ... — keeps Retrofit interfaces but allows their names to be obfuscated; the method annotations are preserved for Retrofit's reflection
  6. 6. @com.google.gson.annotations.SerializedName — keeps any field annotated with @SerializedName, an alternative to keeping entire model packages
  7. 7. -keepclassmembers enum * { values(); valueOf(); } — preserves enum methods required for deserialization; without this, enums break when deserialized from JSON or Bundles
  8. 8. android.os.Parcelable$Creator — preserves the CREATOR field required by the Parcelable contract; without it, unparceling crashes at runtime
  9. 9. @android.webkit.JavascriptInterface — keeps methods callable from WebView JavaScript; obfuscation would make them unreachable from JS code
  10. 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?
Two issues: missing resource shrinking, and model fields are obfuscated by R8 making Gson unable to match JSON keys to field names
Show answer
Bug 1: shrinkResources is not enabled — unused resources bloat the APK. Add isShrinkResources = true. Bug 2: R8 obfuscates UserProfile's fields (userId becomes 'a', displayName becomes 'b'), so Gson cannot match JSON keys 'userId'/'displayName' to the obfuscated names. Fix: Either add @SerializedName("userId") annotations to each field, add -keep class ApiResponse { *; } and -keep class UserProfile { *; } to proguard-rules.pro, or migrate to kotlinx.serialization which uses code generation instead of reflection.

Explain like I'm 5

Imagine you wrote a giant book with 1000 pages, but people only read 200 of those pages. R8 is like a smart editor who rips out the 800 pages nobody reads, renames all the characters to just letters (A, B, C) so the book takes less ink, and removes all the pictures nobody looks at. But if someone has an index card saying 'look up Captain Hook on page 457,' and the editor renamed Captain Hook to 'C' and removed page 457, it breaks! So you give the editor a sticky note: 'Do NOT touch Captain Hook.' That sticky note is a -keep rule.

Fun fact

R8 was developed by Google as a successor to ProGuard. The name 'R8' stands for 'Release 8' — it was the 8th iteration of Google's internal code shrinker experiments. In benchmarks, R8 produces APKs that are 5-10% smaller than ProGuard with 2-3x faster build times. Google Play requires apps targeting API 30+ to use App Bundles, where R8 optimization becomes even more critical since per-ABI and per-density splitting amplifies the savings.

Hands-on challenge

Create a multi-module Android project setup with proper R8 configuration. In the app module, define a Gson-based API response model with nested objects and configure proguard-rules.pro to protect it. In a shared library module, create a public API class and write consumer-rules.pro that preserves it. Add rules for Retrofit interfaces, Parcelable classes, and JavaScript interface methods. Then demonstrate how to use the retrace command to deobfuscate a sample obfuscated stack trace.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery