ProGuard / R8
Every release build of TrailLog ships as a single APK or app bundle containing every class from
every library it depends on, with the exact class and method names the compiler produced — names
like TrailRepository or getTrailsSortedByDate that are perfectly readable
to anyone who unzips the file. R8 is Android's build-time tool for turning that
readable, bulky output into something smaller, faster, and harder to casually read — and it runs
automatically as part of a release build once you switch it on.
R8 and ProGuard: Why the Names Get Confused
ProGuard was the original standalone Java shrinker/obfuscator tool Android projects
used for years. R8 is Google's own replacement — it does the same job, faster, as
part of the Android Gradle Plugin itself, and it has fully replaced ProGuard as the tool that
actually runs in every modern Android project. Here's the part that genuinely confuses people: R8
still reads configuration files written in ProGuard's rule syntax, and Android
Studio still names that file proguard-rules.pro by convention. So when you see
"ProGuard rules" mentioned anywhere in this lesson (or anywhere online), it almost always means
rules fed into R8, not the old standalone tool — the file format outlived the tool that
invented it.
Three Things R8 Does
| Task | What it means for TrailLog |
|---|---|
| Shrinking | Removes classes, methods, fields, and resources nothing actually reaches at runtime — an unused library feature TrailLog never calls simply disappears from the final APK, cutting its size. |
| Obfuscation | Renames surviving classes and methods to short, meaningless names (TrailRepository might become a.b.c) — it doesn't stop determined reverse-engineering, but it removes the easy, readable roadmap an attacker would otherwise get for free. |
| Optimization | Rewrites and restructures bytecode — inlining short methods, removing dead branches — for a measurably faster, leaner runtime footprint, with no visible behavior change. |
Turning R8 On for a Release Build
R8 is off by default for debug builds (you want readable stack traces while developing) and is
enabled per build type in build.gradle.kts:
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
isMinifyEnabled = true is the switch that turns R8 on for that build type.
isShrinkResources = true goes a step further and removes unused resources too — layout
files, drawables, strings nothing references — but it only has an effect once minification is also
on, since it relies on R8's analysis of what code (and therefore what resources) is actually
reachable. proguardFiles(...) lists the rule files R8 reads: Android's own sensible
defaults, plus TrailLog's own proguard-rules.pro.
Keep Rules: Protecting Code R8 Can't Safely Guess About
R8's shrinking and renaming both rely on statically tracing which code is reachable from your app's
entry points. That works perfectly for ordinary method calls — but it breaks down for code reached
through reflection, where a class or property name is looked up as a string at
runtime rather than referenced directly in code R8 can see. kotlinx.serialization, used
back in JSON & kotlinx.serialization to turn
a Trail into JSON and back, does exactly this: it needs the real, un-renamed property
names of an @Serializable class at runtime to match them against JSON keys. If R8
obfuscates Trail's property names without knowing that, serialization silently breaks
in a release build while working fine in debug.
The fix is a keep rule in proguard-rules.pro, telling R8 explicitly:
leave this class, and everything about it, exactly as written.
# proguard-rules.pro
# Keep TrailLog's serializable data classes untouched — kotlinx.serialization
# reads their real property names at runtime through reflection.
-keep @kotlinx.serialization.Serializable class com.codingwithsonu.traillog.model.** { *; }
# Keep the generated serializer companions kotlinx.serialization relies on
-keepclassmembers class com.codingwithsonu.traillog.model.** {
*** Companion;
}
-keepclasseswithmembers class com.codingwithsonu.traillog.model.** {
kotlinx.serialization.KSerializer serializer(...);
}
The -keep rule with { *; } tells R8 to leave every member of every
matching class untouched — no renaming, no removal — which is exactly what a reflection-driven
library needs from its data classes. Rules like this exist specifically for the gap between what R8
can prove is safe by static analysis and what your app actually needs at runtime through reflection,
annotations, or JNI.
build.gradle.kts configuration and proguard-rules.pro syntax are correct, real Android build configuration — but this website's own project doesn't run a live Android Studio build, so nothing here executes on the page itself.Test a Release Build Locally Before You Publish
This is the single most important habit in this lesson: build and manually test a minified release build on a real device or emulator before ever uploading it anywhere. R8-related bugs — a crash from a class that got stripped, a feature that silently breaks because reflection couldn't find a renamed method — routinely don't show up in debug builds at all, since R8 doesn't run there. The failure mode is specifically a release-build problem, so it can only be caught by actually running a release build yourself first, rather than trusting that "it worked in debug" means anything about how it'll behave once shrunk and obfuscated.
Most Libraries Already Ship Their Own Rules
You won't need to hand-write keep rules for every library TrailLog depends on. Most well-maintained
libraries — Retrofit, Room, Gson, and others — bundle their own default R8/ProGuard rules inside
their .aar package, which R8 automatically picks up and applies. Custom keep rules are
mainly needed for your own reflection-dependent code, like TrailLog's serializable models
above, not for well-behaved third-party dependencies.