
Flutter: Mastering ProGuard & R8 Obfuscation
App crashes only in Release mode? It's likely ProGuard/R8. Learn how to debug obfuscated stack traces, use `@Keep` annotations, and analyze `usage.txt`.

App crashes only in Release mode? It's likely ProGuard/R8. Learn how to debug obfuscated stack traces, use `@Keep` annotations, and analyze `usage.txt`.
A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Yellow stripes appear when the keyboard pops up? Learn how to handle layout overflows using resizeToAvoidBottomInset, SingleChildScrollView, and tricks for chat apps.

Push works on Android but silent on iOS? Learn to fix APNs certificates, handle background messages, configure Notification Channels, and debug FCM integration errors.

Think Android is easier than iOS? Meet Gradle Hell. Learn to fix minSdkVersion conflicts, Multidex limit errors, Namespace issues in Gradle 8.0, and master dependency analysis with `./gradlew dependencies`.

It runs perfectly on the emulator and your local device in Debug mode. But the moment you upload the APK/AAB to the Play Store and download it, it crashes instantly on launch or when opening a specific screen. The stack trace (Crashlytics) looks completely garbled:
java.lang.NullPointerException: ... at a.b.c.d(Unknown Source)
at a.b.x.f(Unknown Source)
...
Who are a.b.c.d? Why is the source unknown?
This is the work of R8 (formerly ProGuard). It has shrunk and obfuscated your code to protect your intellectual property and reduce app size, but in doing so, it has broken something vital.
Android release builds typically pass through the R8 Compiler. R8 performs three main tasks to optimize your application:
main() or Activity classes) and removes any code that appears unreachable. This significantly reduces the DEX file size.a, b, c. This makes reverse engineering difficult.The core problem arises when R8 incorrectly identifies code as unused. This happens frequently with:
Class.forName("User")). R8 cannot see this string connection during static analysis.name to a, the JSON parser can't find the name field anymore.Let's trace a common crash scenario. You have a data class for API responses:
data class User(val firstName: String, val lastName: String)
And you use Gson to parse this: gson.fromJson(json, User::class.java).
R8 looks at your code and says: "I see the User class, but no one ever calls User.getFirstName() directly in the Java/Kotlin code. So I will rename firstName to a to save space."
When the JSON comes in {"firstName": "John"}, Gson looks for a field named firstName.
But your class now looks like: class a { String a; String b; }.
Gson can't match "firstName" to "a", so it leaves the field null or throws an error.
@Keep Annotation (The Modern Way)The simplest and most robust fix for your own code is to use the @Keep annotation.
When you annotate a class, method, or field with @Keep, you are explicitly telling R8: "Do not touch this. Do not rename it. Do not remove it."
Modify your Android code (android/app/src/main/kotlin/...):
import androidx.annotation.Keep
@Keep
data class User(val firstName: String, val lastName: String)
R8 respects this annotation and will preserve the class name and its members as is. This is preferred over ProGuard rules because it keeps the configuration right next to the code it protects.
For third-party libraries (which you cannot edit/annotate), you must use a specific configuration file.
Edit android/app/proguard-rules.pro. If it doesn't exist, create it.
# 1. Keep all classes in your data models package
# This uses a wildcard (**) to match subpackages and (*) to match inner members.
-keep class com.example.myapp.models.** { *; }
# 2. Keep Enum values
# Enums are often accessed via valueOf(String), which uses reflection.
-keepclassmembers enum * { *; }
# 3. Protect specific libraries (e.g., Retrofit, Gson)
# Most popular libs have documentation on what rules they need.
-keep class retrofit2.** { *; }
-keep class com.google.gson.** { *; }
# 4. Keep Attributes
# Required for many annotation processors
-keepattributes Signature
-keepattributes *Annotation*
-keepattributes EnclosingMethod
Then, register this file in your android/app/build.gradle:
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization
minifyEnabled true
// Enables resource shrinking (stripping unused drawables/layouts)
shrinkResources true
// Load the default Android rules + your custom rules
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
When R8 obfuscates your code, it generates a "Dictionary" that maps the obfuscated names back to the original names. This is called the Mapping File.
Location: build/app/outputs/mapping/release/mapping.txt
If you receive a crash report saying NullPointer at a.b.c.d, you cannot debug it without this file.
You must upload this mapping.txt to:
Once uploaded, the dashboard will "de-obfuscate" (symbolicate) the stack trace, revealing that a.b.c.d is actually com.example.User.parseData.
Critical Warning: Every build generates a new, unique mapping file. You cannot use the mapping file from Build v1.0 to debug a crash in Build v1.1. Always archive your mapping files with your release artifacts.
R8 can also remove unused resources (images, layouts, strings).
This is controlled by shrinkResources true.
However, if you access resources dynamically (e.g., getResources().getIdentifier("img_" + name, ...)), R8 might delete img_01.png because it doesn't see a direct reference like R.drawable.img_01.
To prevent this, create android/app/src/main/res/raw/keep.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/img_*,@layout/dynamic_*" />
This tells the resource shrinker to keep any drawable starting with img_ and any layout starting with dynamic_.
If your app is crashing in release mode, follow these steps:
flutter run --release). This lets you see the logs in real-time.usage.txt: Located in the same folder as mapping.txt. It lists every class and methods that R8 removed. Search for your missing class here.seeds.txt: This lists items that R8 kept because of your configuration.minifyEnabled false in build.gradle and build release. If it works, it is definitely an R8 configuration issue.-keep class com.my.package.** { *; } aggressively to isolate which package is causing the issue.@Keep or -keep rules for Data classes, Enums, and Reflection targets.mapping.txt. It's your only hope for debugging production crashes.usage.txt. to see exactly what R8 stripped out.Security is important, but a working app is essential. Don't let R8 be overzealous—teach it what matters.