CalcSnippets
Mobile Development 4 min read

Diagnose Flutter Release-Only Missing Classes Before Disabling R8

A Flutter app that works in debug but fails after shrinking may expose reflection, keep-rule, or dependency issues. Reproduce the release variant and inspect its mapping artifacts.

Debug and release are different programs

Flutter debug builds and Android release builds differ in shrinking, obfuscation, resource processing, signing, and optimization. A plugin that relies on reflection may work in debug and fail only after R8 removes a class or member it cannot see statically. Turning off shrinking can confirm the direction, but it is not a finished repair.

Reproduce the exact release artifact:

flutter build apk --release --verbose

Keep the generated mapping and usage reports from the Gradle build. Search the first missing-class or removed-member message and identify which dependency owns that symbol. Confirm the runtime failure with logcat from the release APK; a build warning is not always the user-visible cause.

Add the narrowest maintained rule

If a library documents required keep rules, use its supported consumer rules or add a narrowly scoped rule to the app's ProGuard configuration. Avoid broad rules that preserve every class in the application; they can erase the size and optimization benefits and hide future dependency problems.

Compare the release dependency graph with debug only when the build files explicitly define different configurations. Check plugin release notes and verify that the required implementation is packaged. If the class is absent before shrinking, a keep rule cannot create it; fix the dependency or variant selection instead.

Test the final signed or signing-equivalent artifact on a clean emulator and exercise the feature that failed. Keep a regression test or smoke check for reflection-heavy paths. The useful outcome is not merely “release now launches,” but evidence that the required classes remain while unrelated code is still eligible for optimization.

Follow the missing symbol back to its owner

Start with the exact class name and the first diagnostic that mentions it. Determine whether the symbol should come from an app module, a transitive library, a generated source set, or an optional product flavor. Inspect the release runtime classpath and the artifact's contents before adding a keep rule. If the dependency is not packaged at all, a shrinker rule cannot restore it; correct the variant dependency or plugin setup. If it is present before shrinking and removed during optimization, then a narrow keep rule or an upstream library fix may be appropriate.

Reflection makes this distinction important. A library can load a class by a string name or discover annotations at runtime, patterns that static analysis may not see. Prefer documented consumer rules supplied by the library. If you must add an app rule, scope it to the exact package or annotated members and explain why the reflective access cannot be expressed statically. Avoid blanket -keep class ** rules: they can preserve a large portion of the app, increase download size, and conceal future shrinker incompatibilities.

Compare the exact release variant and runtime behavior

Build the same flavor, minification setting, and signing mode used for distribution. A generic release command may select a different variant than CI. Save the mapping file and relevant R8 reports as build artifacts, because obfuscated stack traces are much harder to interpret after a crash. Reproduce on a clean emulator or device with the release APK, then collect logcat around the failure. Confirm that the reported missing class corresponds to the user-visible failure rather than an unrelated warning.

If disabling shrinking makes the symptom disappear, use that as a temporary experiment and restore shrinking for the fix. Test both the affected feature and app startup afterward. Keep a small smoke test for reflection-heavy flows such as serialization, dependency discovery, or plugin registration. An upgrade to a library can change its consumer rules, so review the release behavior when updating those dependencies.

Treat optimization as a measurable requirement. Compare artifact size and startup or feature behavior before and after the rule change. A successful launch with a huge keep rule may trade away the intended optimization and leave the underlying dependency issue unresolved. The durable fix keeps required code, leaves unrelated code optimizable, and produces a debuggable mapping artifact for the exact release.

If the library is maintained outside your team, send the smallest reproduction and release mapping evidence upstream. An upstream consumer rule benefits every application and is easier to maintain than a permanent app-specific exception. Until that release exists, track the local rule with the dependency version and remove it when the fix lands. Re-run the same release smoke test after each library upgrade because shrinker behavior can change as consumer rules evolve.

Keep the mapping and usage reports attached to failed release builds. They allow a later crash report to be symbolicated and show whether a keep rule preserved more code than intended. Treat shrinking as part of the release contract: test the optimized artifact, compare its size, and document every reflection-based dependency that needs special handling.

Keep reading

Related guides