Skip to content
All posts
May 31, 20264 min read

ProGuard and R8: How to Configure Code Shrinking Without Breaking Your App

R8 is enabled in every release build but most developers just hope it works. When it breaks something, debugging is painful. Here's how to configure ProGuard rules correctly, debug obfuscation issues, and keep your app running after shrinking.

AndroidToolsRelease
Share:

R8 is one of those things that works silently until it doesn't. You ship a release build, something crashes, and the stack trace is full of single-letter class and method names. Here's how to prevent and fix these problems.


What R8 Does

R8 performs three operations in one pass:

  1. Shrinking — removes unused classes, methods, and fields
  2. Obfuscation — renames classes and methods to shorter names (
    code
    TaskRepository
    code
    a
    )
  3. Optimization — inlines short methods, removes dead code paths

All three happen automatically in release builds when

code
isMinifyEnabled = true
.


When R8 Breaks Things

R8 analyzes code statically — it looks at what's called in code it can see. It can't see:

  • Reflection
    code
    Class.forName("com.yourapp.SomeClass")
    references a class by string name. R8 might rename or remove
    code
    SomeClass
    .
  • Serialization/deserialization — Gson, Moshi, or Kotlinx Serialization need field names to match JSON keys. If R8 renames fields, deserialization fails.
  • Third-party libraries using reflection — many libraries use reflection internally.
  • Classes referenced only from XML — custom views, custom attributes.

Reading Crash Stack Traces After Obfuscation

Obfuscated crashes look like:

code
java.lang.NullPointerException
  at a.b.c.d(Unknown Source:12)
  at e.f.g.h(Unknown Source:5)

To de-obfuscate:

bash
# Using the retrace tool
java -jar retrace.jar \
    app/build/outputs/mapping/release/mapping.txt \
    stacktrace.txt

Or in Android Studio: Analyze → Stack Trace... → paste the obfuscated trace → click "Deobfuscate."

Always save mapping files. The mapping file connects obfuscated names to original names. R8 generates it at

code
app/build/outputs/mapping/release/mapping.txt
. Archive it with every release — you'll need it to debug production crashes from that version.


Writing ProGuard Rules

The

code
proguard-rules.pro
file tells R8 what to keep:

Keep Classes Referenced by Reflection

code
# Keep the full class name
-keep class com.yourapp.data.models.** { *; }

# Keep specific class
-keep class com.yourapp.SomeClass { *; }

# Keep class members (fields and methods)
-keepclassmembers class com.yourapp.SomeClass {
    public <fields>;
    public <methods>;
}

Keep Serialization Models

For Kotlinx Serialization:

code
-keepattributes *Annotation*
-keepclassmembers @kotlinx.serialization.Serializable class ** {
    *** Companion;
    kotlinx.serialization.KSerializer serializer(...);
}

For Gson:

code
-keepclassmembers class com.yourapp.data.dto.** {
    <fields>;
}

Keep Custom Views

code
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
}

Using @Keep Annotation

Annotate specific classes or methods instead of writing rules:

kotlin
@Keep
data class TaskDto(
    val id: String,
    val title: String,
    val completed: Boolean
)

class TaskRepository {
    @Keep
    fun getTask(id: String): Task? { ... }
}

code
@Keep
prevents R8 from shrinking or renaming the annotated element. Good for targeted keep without broad rules.


Debugging R8 Issues Systematically

When a release build crashes but debug doesn't:

Step 1: Run the release build with

code
minifyEnabled = true
but
code
debuggable = true
. This gives you readable stack traces from the release build:

kotlin
buildTypes {
    create("releaseDebug") {
        initWith(getByName("play store"))
        isDebuggable = true
        applicationIdSuffix = ".releasedebug"
    }
}

Step 2: Use R8's

code
-printusage
and
code
-whyareyoukeeping
to understand what R8 is removing:

code
# In proguard-rules.pro (temporarily, for debugging)
-printusage build/r8-usage.txt
-whyareyoukeeping class com.yourapp.MyClass

Step 3: Binary search. Disable obfuscation first:

kotlin
// Temporarily disable obfuscation (keep shrinking)
isMinifyEnabled = true
proguardFiles(
    getDefaultProguardFile("proguard-android-optimize.txt"),
    "proguard-rules.pro"
)

Add to proguard-rules.pro:

code
-dontobfuscate

If the crash goes away, the issue is obfuscation (field/class name change). If it persists, the issue is shrinking (class or method removed).


Library Consumer Rules

Good libraries ship with their own ProGuard rules that R8 applies automatically (via

code
proguard.txt
in the AAR). You don't need to write rules for:

  • Retrofit
  • OkHttp
  • Room
  • Hilt
  • Coil

Libraries that require manual rules in your

code
proguard-rules.pro
:

  • Any library using reflection on your model classes
  • Custom serialization frameworks
  • Native bridge classes

Checking What Was Removed

bash
./gradlew assembleRelease

# View what R8 removed
cat app/build/outputs/mapping/release/usage.txt | head -100

The

code
usage.txt
file lists everything R8 removed. If you see a class you need, add a keep rule.


Production-Ready ProGuard Template

code
# Keep Kotlin metadata
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable

# Keep enum members
-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 serializable classes
-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();
}

Takeaways

  • Always save mapping files with each release — de-obfuscation requires the mapping from that specific build
  • Reflection, serialization, and XML-referenced classes need explicit keep rules
  • code
    @Keep
    annotation is cleaner than broad rules for targeted preservation
  • When debugging R8 issues: disable obfuscation first to narrow down the cause
  • Good libraries include their own ProGuard rules — don't duplicate them
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus