Skip to content

[Hilt KSP] @HiltViewModel(assistedFactory=...) fails when the factory is generated by another KSP processor in the same round #5160

@matteo-goghero-leitha

Description

@matteo-goghero-leitha

Description

When using Hilt with KSP, @HiltViewModel(assistedFactory = GeneratedFactory::class) fails if GeneratedFactory is generated by a custom KSP processor running in the same KSP compilation pass. Hilt emits a fatal error instead of deferring to the next round:

[Hilt] Class MyViewModelFactory is not annotated with @AssistedFactory.
AssistedInjectProcessingStep was unable to process 'MyViewModel(java.lang.String)'
because 'MyViewModelFactory' could not be resolved.
    => type (ERROR annotation value type): MyViewModelFactory

The custom processor does generate the file with @AssistedFactory in the same round (confirmed via logging), but KSP does not make files generated by one processor visible to other processors until the next round. Hilt should defer processing of this element to the next round, but does not.

Root Cause

In ViewModelMetadata.create(), the assistedFactory type reference is resolved and immediately validated:

val assistedFactoryType = viewModelElement
    .requireAnnotation(AndroidClassNames.HILT_VIEW_MODEL)
    .getAsType(ASSISTED_FACTORY_VALUE)
val assistedFactory = assistedFactoryType.typeElement!!  // NPE or ErrorType

ProcessorErrors.checkState(
    assistedFactory.hasAnnotation(ClassNames.ASSISTED_FACTORY),
    viewModelElement,
    "Class %s is not annotated with @AssistedFactory.", ...
)

ProcessorErrors.checkState throws IllegalStateException. In BaseProcessingStep.process(), only ErrorTypeException triggers deferral:

if (e instanceof ErrorTypeException && !isLastRound) {
    elementsToReprocessBuilder.add(element);  // deferred
} else {
    errorHandler.recordError(e);  // fatal
}

Since IllegalStateException is not ErrorTypeException, the error is recorded as fatal and never retried.

Expected Behavior

Hilt should defer validation of the assistedFactory reference when:

  • The type is an ErrorType (not yet generated), OR
  • The type exists but is not yet fully annotated (generated by another processor in the same round)

This would allow the generated factory to become visible in the next KSP round, where Hilt can successfully validate it.

Reproduction

Minimal reproduction project: repro.zip

./gradlew :app:kspDebugKotlin

Build Output

e: [ksp] /Users/.../app/src/main/java/com/example/repro/MyViewModel.kt:25: [Hilt] Class MyViewModelFactory is not annotated with @AssistedFactory.
w: [ksp] [factory-generator] Generated MyViewModelFactory
e: [ksp] AssistedInjectProcessingStep was unable to process 'MyViewModel(java.lang.String)' because 'MyViewModelFactory' could not be resolved.

Project Structure

  • :factory-generator — Minimal KSP processor

    • Reads @GenerateFactory annotation
    • Generates @AssistedFactory interface
    • Uses standard KSP APIs
  • :app — Android app

    • Contains @HiltViewModel with assistedFactory parameter
    • References the generated factory

ViewModel Code

@GenerateFactory
@HiltViewModel(assistedFactory = MyViewModelFactory::class)
class MyViewModel @AssistedInject constructor(
    @Assisted val itemId: String,
) : ViewModel()

Generated Factory

@AssistedFactory
interface MyViewModelFactory {
    fun create(itemId: String): MyViewModel
}

The custom processor logs that it successfully generated the file:

w: [ksp] [factory-generator] Generated MyViewModelFactory

However, Hilt fails to see it because KSP processors do not have visibility into files generated by other processors in the same round.

Context: Why This Worked with KAPT

With KAPT, this pattern worked because:

  1. KSP run first → custom processor generates factory as .kt file
  2. KAPT runs second → Hilt processes compiled .class files where factory is already resolved
  3. Clear ordering ensured factory was visible to Hilt

With Hilt migrated to KSP, both processors run in the same pass with no guaranteed ordering. KSP spec states that files generated by processor N become visible to processor M only in round N+1.

Suggested Fix

In ViewModelMetadata.create(), wrap the assistedFactory resolution and validation to throw ErrorTypeException (or allow deferral) when:

  • The type is an ErrorType
  • OR typeElement is null
  • OR the annotation check fails but the type might be generated in the next round

This would let BaseProcessingStep's existing deferral mechanism handle the retry in the next KSP round.

Environment

  • Hilt: 2.59.2
  • KSP: 2.3.7
  • Kotlin: 2.3.20
  • AGP: 9.1.1
  • Gradle: 9.3.1

repro.zip

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions