๐Ÿงช Testing, Debugging, and Performance

ProGuard and R8 Explained

R8 is the compiler step that makes Android release builds smaller and faster. It also causes the classic 'works in debug, crashes in release' bug. Understanding what R8 does, and when to write keep rules, removes the mystery.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. What R8 does
  2. Enabling R8
  3. When you need keep rules
  4. "Works in debug, crashes in release"
  5. Mapping files and readable crashes
  6. What you gain
  7. Common mistakes
  8. Frequently asked questions
  9. Conclusion

ProGuard was the original Java shrinker and obfuscator. On Android it has been replaced by R8, which reads ProGuard-format rules but does more. It's integrated into the build, and it converts code to DEX while optimising it. You enable it for release builds, and occasionally you have to tell it what not to touch.

What R8 does

R8 processing steps Starting from entry points declared in the manifest and keep rules, R8 traces reachable code and removes the rest (shrinking), optimises code by inlining and removing dead branches, renames classes and members to short names (obfuscation), and with resource shrinking removes unused resources, producing a smaller DEX. Entry pointsmanifest + keep rules Shrinkremove unused Optimizeinline, simplify Obfuscate + DEXshort names, smaller app Anything reached only via reflection is invisible to the tracing step, which is why keep rules exist.
R8 keeps what it can prove is used. Reflection breaks that proof.

Enabling R8

app/build.gradle.ktsGradle KTS
android {
    buildTypes {
        release {
            isMinifyEnabled = true          // shrink + optimize + obfuscate code
            isShrinkResources = true        // remove unused resources (requires minify)
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro",
            )
        }
    }
}

When you need keep rules

Most modern libraries ship their own consumer rules, so you need few custom rules. Typical cases:

  • Reflection: classes or members accessed by name.
  • Serialization libraries that use reflection, for example Gson models. kotlinx.serialization and Moshi codegen generate code and need far fewer rules.
  • JNI: native code calling Java/Kotlin methods by name.
  • Classes named in non-manifest XML or strings and loaded dynamically.
proguard-rules.proText
# Keep models deserialized reflectively by Gson (fields read by name)
-keep class com.example.api.legacy.model.** { <fields>; }

# Keep a class loaded by name via reflection
-keep class com.example.plugins.PdfExporter { public <init>(); }

# Keep JNI-called methods
-keepclasseswithmembernames class * { native <methods>; }
Be specific

Broad rules like -keep class com.example.** { *; } disable most of R8's benefits for your code. Keep the minimum: the classes or members actually accessed reflectively.

"Works in debug, crashes in release"

  1. Reproduce with a release-like build locally (minified, debuggable off). Or create a staging build type with minify enabled.
  2. Read the obfuscated stack trace and decode it with the mapping file (see below).
  3. Look for reflection at the crash site: missing class or method, null fields after JSON parsing, or ClassNotFoundException.
  4. Add a narrow keep rule or switch to a codegen-based library, then re-test.

Mapping files and readable crashes

Obfuscation renames classes (a.b.c), so crash reports need the mapping file (build/outputs/mapping/release/mapping.txt) to be readable:

  • Upload it with each release. Google Play Console and Crashlytics deobfuscate automatically when the mapping is provided (Gradle plugins can upload it).
  • Archive mapping files per version. You can't recreate them later.
  • Decode traces manually with the retrace tool when needed.

What you gain

Smaller app

Unused library code and resources are removed, often a substantial size reduction.

Faster code

Inlining, class merging and dead-code removal reduce runtime work.

Harder to reverse engineer

Obfuscation raises the effort, but it is not security. Never rely on it to hide secrets.

Common mistakes

  • Never testing a minified build before release.
  • Disabling minification to "fix" a crash instead of writing a targeted rule.
  • Losing mapping files, so production crashes become unreadable.
  • Believing obfuscation protects API keys. It doesn't. See Secure API Keys.

Frequently asked questions

Is ProGuard still used?

R8 is the default for Android builds. It understands ProGuard rule syntax, so "ProGuard rules" files still apply.

What is R8 full mode?

A more aggressive optimisation mode that is now the default in recent AGP versions. It can expose reflection issues that older modes tolerated.

Should debug builds be minified?

Usually not, for build speed and debuggability. Test a minified build on CI or in a staging build type instead.

Conclusion

R8 shrinks, optimises and obfuscates release builds by keeping only reachable code. Enable it with resource shrinking, write narrow keep rules only for reflection and JNI, test minified builds before release, and archive and upload mapping files.

Next step: go further with How to Reduce Android App Size.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Performance work starts with understanding how Android apps are built and how they behave at runtime. Strengthen those fundamentals and the profiling tools start to make sense.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp