ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-06-22Advanced

It Only Crashes in Release — Tracking Down When R8 Full Mode Broke My Gson Mapping

It works in debug but crashes only in release. This walks through the R8 full-mode pattern that breaks Gson reflection, how to reproduce it locally, deobfuscate the stack trace, and write the smallest keep rules that fix it.

android28r8proguardgsonrelease-build

Premium Article

One morning Crashlytics was full of a NullPointerException I'd never seen. The top of the stack trace was obfuscated symbols like a.b.c.d — class names I never wrote. Running a debug build on my own device reproduced nothing. A few hours after release, only users who opened one particular screen were crashing. As an indie developer who has run ad-supported wallpaper apps for years, this "only crashes in release" category of bug has tripped me up more than once.

The cause is almost always in the same place. R8's full-mode obfuscation rewrites the field names of the data classes that Gson reads and writes via reflection, so the mapping to the JSON keys falls apart. The debug build has obfuscation disabled, so it runs fine; only release breaks. That asymmetry is exactly what makes it so hard to reproduce, and why you can burn hours before you find it.

This article is a record of how I tracked the problem down the last time I hit it, and how I ultimately fixed it. "Just keep everything and it goes away" is technically true, but that's abandoning the design decision — you throw away both the obfuscation and the size reduction at the same time. We'll go all the way to protecting only the broken class, as narrowly as possible.

Why it never happens in debug and only crashes in release

In an Android release build, when minifyEnabled true, R8 performs shrinking (removing unused code) and obfuscation (shortening class, method, and field names). Since AGP 8.0, R8 is the standard code shrinker, and "full mode" is enabled by default. Full mode optimizes more aggressively than the legacy ProGuard-compatibility mode: anything not explicitly protected by a keep rule is removed or renamed more freely.

The trouble is that Gson matches fields by name. When mapping {"display_name": "Sunset"} onto data class Category(val displayName: String), Gson reads the field name via reflection at runtime, and absent an annotation it uses the field name itself as the JSON key. But if R8 has renamed that field to a, what Gson sees is a, not displayName. The JSON key never matches, and the value stays unfilled as null.

In the debug build, minifyEnabled false, so the field names stay intact and nothing breaks. That asymmetry is the real source of the difficulty. As long as the person who wrote the code runs it on their own machine, it almost never reproduces.

Reproduce the release behavior locally

Killing the cause by guesswork is slow. The fast path is to first make the release behavior reproduce reliably on your own machine.

# Build the release variant and put it on a device/emulator
./gradlew assembleRelease
 
# Install the signed release APK (adb install works even if it isn't debuggable)
adb install -r app/build/outputs/apk/release/app-release.apk

Now the same obfuscated code that ships to production runs on your machine too. "Only crashes in release" makes it feel like you can only investigate in production, but a single assembleRelease puts you in a state where you can reproduce it on your own device as many times as you like. There was a period where I skipped this and just stared at production logs, and that was the single biggest waste of time.

Open the screen that crashes, trigger it, and capture the stack trace with adb logcat. The trace at this point is still obfuscated symbols, so the next step is to translate it back to the original names.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
You can reproduce a release-only crash on your own machine instead of staring at production logs
You'll understand why R8 full mode breaks Gson reflection, and how to restore an obfuscated stack trace with mapping.txt and retrace
You can stop hiding behind 'keep everything' and protect only the broken class with minimal keep rules in your own app
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-07-18
After Compose-First: Choosing Which View Screens to Migrate, Ranked by Churn Instead of Count
Google has declared Android development Compose-first. Here is how I rank View-based screens for migration using git history rather than screen counts, with the scoring script I actually ran and the three places partial migration quietly duplicates state.
App Dev2026-07-03
Catching Download Size Regressions Before Submission Day — A Weekly Agent Gate for AAB/IPA Size Budgets
Mediation SDKs and bundled assets quietly inflate download size. A design for size ledgers, budget gates, and agent-driven delta attribution using bundletool and App Thinning reports.
App Dev2026-06-30
The App Open Ad Antigravity Wrote for Me Fires Every Time I Return From My Own Paywall or Rewarded Video
Ask Antigravity to add an App Open ad and it shows one the instant you return from your own rewarded video or the Google Play purchase sheet — which also brushes against AdMob policy. Here is a foreground arbiter that records why the app came back, with working Kotlin and a verification matrix.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →