Debugging R8 Keep Rules With the Files It Already Writes for You
missing_rules.txt, configuration.txt, -whyareyoukeeping and -checkdiscard, and which one to open for which problem.
Note: drafted with AI assistance and checked against the official documentation.
Most R8 problems show up in one of three ways. The release build refuses to build. The release build installs fine and then crashes or quietly misbehaves. Or the build works, but R8 kept a lot more code than you expected and the app is bigger than it should be.
The good news is that R8 and the Android Gradle plugin already write most of what you need to debug all three. The files sit in app/build/outputs/mapping/<variant>/ and a lot of people never open them. Here’s which file to look at for which problem, and a couple of rules that are only meant for debugging.
The build fails with “Missing classes detected”
Since AGP 8.0, a class that R8 can’t find is a build error, not a warning. The message points you to a file AGP generates for you:
app/build/outputs/mapping/release/missing_rules.txtInside you’ll find a list of -dontwarn lines, one per missing class. The common move is to paste the whole thing into proguard-rules.pro and move on. Sometimes that’s right. Sometimes it hides a real problem.
The AGP release notes give two normal reasons to ignore a missing class. The first is a library written for the JVM that references JVM only classes, for example java.lang.instrument.ClassFileTransformer. The second is a dependency that uses an API which is only there at compile time. In both cases a targeted rule is fine:
-dontwarn java.lang.instrument.ClassFileTransformerIf the missing class is something your app actually calls at runtime, a -dontwarn just moves the failure from build time to a user’s phone. In that case the fix is to add the missing dependency. So read the list before pasting it. If a class name surprises you, find out who references it first.
There’s also the -ignorewarnings escape hatch to get the old AGP 7 behavior back. The docs say it’s not recommended, and I agree. It silences every missing class at once, including the ones you’d want to know about.
The app crashes only in release
When something works in debug and breaks with R8 on, the cause is usually reflection. R8 can only see static references. If code is reached through Class.forName(), Something::class.constructors, kotlin.reflect or java.lang.reflect, R8 may decide it’s unused and remove or rename it.
The exceptions give it away: ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, NoClassDefFoundError, NoSuchMethodError or NoSuchFieldError.
Before you write any rule, get a readable stack trace. From Android Studio Otter 3 Feature Drop and AGP 9.0, Logcat deobfuscates stack traces for you. On older setups, use retrace with the mapping file from that exact build:
$ANDROID_HOME/cmdline-tools/latest/bin/retrace \
app/build/outputs/mapping/release/mapping.txt trace.txtmapping.txt gets overwritten on every build, so keep a copy for every version you ship. Without the right one, crash reports from that release stay unreadable.
Get raj kavadia’s stories in your inbox
Join Medium for free to get updates from this writer.
Once you know what was removed, keep only that. If a class is created by name and only needs its no-arg constructor, say so:
-keep class com.example.export.CsvExporter {
<init>();
}That’s much better than -keep class com.example.export.** { *; }, which stops R8 from shrinking, optimizing or renaming the whole package. If the reflection is in your own code, the docs also suggest the Jetpack Keep annotations instead of a rule file, and their lint checks flag other reflective calls that aren’t annotated yet.
R8 kept more than you expected
This one is harder because nothing is broken. The app just isn’t as small or as optimized as it could be. Keep rules come from your app, from every library you depend on, and from rules AAPT generates for classes referenced in XML. Any of them can be the reason.
Start with the merged view. R8 writes every rule it applied, from every module and library, to configuration.txt under app/build/outputs/mapping/. Search it for wide wildcards like { *; } on whole packages. Each rule in that file comes from somewhere, so you can usually trace a broad rule back to the library that shipped it.
When one specific class refuses to go away, ask R8 directly. Add this temporarily:
-whyareyoukeeping class com.example.foo.MainActivity {
private void setLabel(...);
}The answer is printed in the build output. It shows the shortest path from a kept item to the thing you asked about, like this:
com.example.foo.MainActivity
|- is referenced in keep rule:
| /app/build/intermediates/aapt_proguard_file/release/processReleaseResources/aapt_rules.txt:4:1
Nothing is keeping void com.example.foo.MainActivity.setLabel()One thing to watch: it only reports one reason, the shortest path. Remove that path and the item can still be kept for a different reason, so run it again after each change.
Make the build fail when something isn’t removed
-whyareyoukeeping answers a question once. -checkdiscard turns the answer into a check. If the class or member you name is still in the output, the build fails.
That’s handy for code that must never ship, like a debug menu that’s only reached from debug builds:
-keep,allowshrinking class com.example.debug.DebugMenu { *; }
-checkdiscard class com.example.debug.DebugMenuThe first line matters. Without it, R8 can inline a class’s methods somewhere else, so the class name disappears while its code is still there. -keep,allowshrinking tells R8 it may remove the class but not merge, inline or rename it. Together, the two lines only pass if none of that class’s code is left.
If you’re on AGP 9.3
AGP 9.3.0 added the R8 Configuration Analyzer. You can run it without building an APK:
./gradlew :app:analyzeReleaseR8ConfigIt writes an HTML report to app/build/reports/r8/r8-config-analyzer-release.html, with shrinking, optimization and obfuscation scores and a breakdown of what each keep rule blocks. It also flags rules that match nothing, duplicates, and broad rules that make a narrower one pointless. That’s most of the digging you’d otherwise do by hand in configuration.txt.
Quick map
Build won’t finish: read missing_rules.txt, then decide per class. Crash in release: retrace with the matching mapping.txt, then write the narrowest keep rule you can. Too much kept: configuration.txt, -whyareyoukeeping, and the analyzer if you have it. Code that must never ship: -checkdiscard.
Take -whyareyoukeeping out once you have your answer. -checkdiscard can stay, because that one works best sitting in CI.










