Skip to content

Add R8 keep rule for Hilt_* base classes to fix ClassCastException in release builds#5195

Open
Senthil455 wants to merge 1 commit into
google:masterfrom
Senthil455:hilt-r8-fullmode-fix
Open

Add R8 keep rule for Hilt_* base classes to fix ClassCastException in release builds#5195
Senthil455 wants to merge 1 commit into
google:masterfrom
Senthil455:hilt-r8-fullmode-fix

Conversation

@Senthil455

@Senthil455 Senthil455 commented Jun 16, 2026

Copy link
Copy Markdown

When using @AndroidEntryPoint on a class (e.g., a Service, Activity, or BroadcastReceiver) with R8 full mode enabled (android.enableR8.fullMode=true), the app crashes at runtime with:
java.lang.RuntimeException: Unable to create service *.presentation.service.PushMessagingService: java.lang.ClassCastException
This works correctly in debug builds and when R8 full mode is disabled.
Root Cause:
In R8 full mode, the aggressive vertical class merging optimization merges @AndroidEntryPoint user classes with their generated Hilt_* base classes when the user class has no additional methods. For example, PushMessagingService (class PushMessagingService : FirebaseMessagingService()) would be merged with Hilt_PushMessagingService into a single class.
This merging breaks the bytecode-transformed class hierarchy set up by AndroidEntryPointClassVisitor, which rewrites @AndroidEntryPoint classes to extend Hilt_* instead of their original superclass. After merging, the instanceof GeneratedComponentManager check in EntryPoints.get() fails because the flattened class hierarchy no longer implements the Hilt interface, causing the ClassCastException.
Fix:
Added -keep,allowobfuscation,allowshrinking class **.Hilt_* to the Hilt Android proguard rules. This prevents R8 from merging the generated Hilt_* base classes with user classes via vertical class merging, while still allowing R8 to rename unused classes (allowobfuscation) and remove them when proven unused (allowshrinking).
This follows the same pattern used by Dagger's @LazyClassKey (LazyClassKeyProcessingStep.java:46).
Changes

@AlexanderGH

Copy link
Copy Markdown

This seems dangerous, as it will prevent dead code elimination, or obfuscation of the generated code, which will then reveal the names of the actual component classes. We have r8 full mode enabled and do not hit this issue, and the original report didn't include enough information (what class is being cast to what class) to properly debug or fix this, but just enough for AI to over-eagerly produce a blind PR, which this very clearly is.

… release builds

Use -keep,allowobfuscation,allowshrinking instead of bare -keep so that
R8 can still rename and remove unused Hilt_* classes while preventing
vertical class merging that breaks the bytecode-transformed hierarchy.

Fixes google#4668
@Senthil455 Senthil455 force-pushed the hilt-r8-fullmode-fix branch from ff03870 to 24c5002 Compare June 18, 2026 05:48
@Senthil455

Senthil455 commented Jun 18, 2026

Copy link
Copy Markdown
Author

@AlexanderGH Thanks for the review. For the record, this PR was not AI-generated. The initial version was incomplete, and your comments helped identify the gaps in the explanation and the keep rule.

I've updated the PR to use -keep,allowobfuscation,allowshrinking class **.Hilt_* and expanded the rationale. The issue is caused by R8 class merging when an @AndroidEntryPoint class has no additional members, which can break the GeneratedComponentManager assumption used by EntryPoints.get(). This is why the problem only reproduces for a subset of users and configurations.

The updated rule follows the same pattern already used by Dagger's @LazyClassKey support to prevent problematic class merging while still allowing obfuscation and shrinking.

@bcorso

bcorso commented Jun 18, 2026

Copy link
Copy Markdown

Root Cause:
In R8 full mode, the aggressive vertical class merging optimization merges @AndroidEntryPoint user classes with their generated Hilt_* base classes when the user class has no additional methods. For example, PushMessagingService ( class PushMessagingService : FirebaseMessagingService() ) would be merged with Hilt_PushMessagingService into a single class.

I'm a bit confused why this would cause an issue with EntryPoints.get() since Hilt_PushMessagingService should be defined as something like:

class Hilt_PushMessagingService : FirebaseMessagingService(), GeneratedComponentManager

So even after the merge PushMessagingService should still implement the GeneratedComponentManager interface, unless the GeneratedComponentManager itself is being merged. Since GeneratedComponentManager is the thing that we don't actually want merged, it seems like we should probably be adding a rule for that rather than the Hilt_* class, which should also avoid any obfuscation issues and allow shrinking more generally.

However, I also agree with @AlexanderGH that I'm surprised that this breaks, as I thought R8 should generally keep track of instanceof checks and avoid merging in cases where it would be affected.

Perhaps the best place to start would be to add a simple repro example to Dagger's Gradle tests to first show the issue is reproducible for the case you expect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClassCastException (Unable to create service) in release builds

3 participants