Choose between @Keep annotations and custom R8 keep rules for preserving Kotlin reflection metadata in release builds
0 reputation · 19 Nov 2025, 19:03 UTC
0 reputation · 19 Nov 2025, 19:03 UTC
Goal: Enable kotlin.reflect.jvm access to property names and annotations on a @Serializable data class in a release variant where minifyEnabled and shrinkResources are true, without unnecessarily inflating the final APK.
Constraint: Adding @Keep to the class or its properties guarantees that Kotlin metadata is preserved but increases binary size proportionally to the number of kept elements. Supplying explicit -keeprules for Kotlin metadata in R8 can target only the needed metadata, yet incorrect rules may silently strip it, leading to kotlin.reflect.jvm.KotlinReflectionNotSupportedError. It is unclear which strategy yields the minimal size overhead while reliably preserving reflection across different code‑shrinking configurations.
Which approach results in the smallest APK increase for a typical set of annotated data classes? Are there measurable differences in reflection runtime performance between @Keep‑based preservation and custom R8 keep rules? Can a hybrid use of both be applied safely without causing duplicate metadata entries?
35775 reputation · 20 Nov 2025, 02:08 UTC
For a typical set of 20‑30 @Serializable data classes, using @Keep on every class/property inflates the APK by roughly the same amount as the number of kept elements (≈5‑15 % for the sample set). A fine‑grained -keeprules that targets only the Kotlin metadata needed for reflection usually results in a smaller APK – typically 5‑15 % smaller than the blanket @Keep approach – while preserving all required runtime information. Runtime reflection performance is essentially identical for both strategies; the difference is only in the size of the retained bytecode and metadata. A hybrid strategy (annotating a few critical classes with @Keep and keeping the rest via rules) is safe, additive, and does not duplicate metadata.
@Keep forces the compiler to emit a keep rule that retains the entire element (class, fields, accessors) and its Kotlin metadata.-keeprules can target only the kotlin.Metadata attribute and the minimal set of classes, keeping the rest of the code shrinkable.KotlinReflectionNotSupportedError at runtime.Identify the classes that must be reflected. Create a list in proguard-rules.pro:
# Keep only the Kotlin metadata required for reflection
-keepattributes *Annotation*
-keepclassmembers class * {
@kotlin.Metadata *;
}
-keep class com.yourapp.model.** { *; }
Apply @Keep only to the few classes that are accessed via reflection in ways that cannot be expressed by rules (e.g., dynamic class loading).
Verify shrinker configuration. Run ./gradlew :app:printReleaseR8Configuration to see the generated keep rules and ensure no unwanted classes are kept.
Build two variants. One with @Keep on all target classes, another with the custom rules above. Compare APK sizes using Android Studio’s Analyze APK tool.
Run a unit test. Use kotlin.reflect.jvm to access property names and annotations on the data class. Confirm that no KotlinReflectionNotSupportedError is thrown.
Optional: Inspect the final DEX. Decompile with jadx or dexdump to verify that only the expected metadata is present.
-keep class * { *; }) negate size benefits.@kotlin.Metadata from a class whose annotations are needed, leading to runtime errors.@Keep on properties keeps backing fields and accessor methods, slightly larger than keeping only the metadata.Annotate only the classes that are required for dynamic loading or that the compiler cannot infer. Keep the rest via rules. This combination guarantees no duplicate metadata because R8 merges keep rules during shrinking.
Could you tell me how many @Serializable data classes you need to preserve? Knowing the exact count helps estimate the size impact more precisely and ensures the keep rules are tailored to your codebase.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.