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
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:
- KSP run first → custom processor generates factory as
.kt file
- KAPT runs second → Hilt processes compiled
.class files where factory is already resolved
- 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
Description
When using Hilt with KSP,
@HiltViewModel(assistedFactory = GeneratedFactory::class)fails ifGeneratedFactoryis 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:The custom processor does generate the file with
@AssistedFactoryin 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(), theassistedFactorytype reference is resolved and immediately validated:ProcessorErrors.checkStatethrowsIllegalStateException. InBaseProcessingStep.process(), onlyErrorTypeExceptiontriggers deferral:Since
IllegalStateExceptionis notErrorTypeException, the error is recorded as fatal and never retried.Expected Behavior
Hilt should defer validation of the
assistedFactoryreference when:ErrorType(not yet generated), ORThis 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
Build Output
Project Structure
:factory-generator— Minimal KSP processor@GenerateFactoryannotation@AssistedFactoryinterface:app— Android app@HiltViewModelwithassistedFactoryparameterViewModel Code
Generated Factory
The custom processor logs that it successfully generated the file:
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:
.ktfile.classfiles where factory is already resolvedWith 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 theassistedFactoryresolution and validation to throwErrorTypeException(or allow deferral) when:ErrorTypetypeElementis nullThis would let
BaseProcessingStep's existing deferral mechanism handle the retry in the next KSP round.Environment
repro.zip