The transition from KAPT (Kotlin Annotation Processing Tool) to KSP (Kotlin Symbol Processing) is no longer optional. With Google formally deprecating KAPT and KSP offering up to 2x faster build speeds, the migration is inevitable.
However, the path is rarely smooth. The most common hurdle during refactoring is a successful Gradle sync followed immediately by build failures. Errors like Unresolved reference, missing generated classes (e.g., DaggerHiltApplication_HiltComponents), or Room databases failing to verify schemas are rampant.
This guide provides a rigorous technical solution to these migration failures, focusing on the root causes within the build toolchain and the specific configuration changes required for Room and Hilt.
The Root Cause: Why KAPT Logic Breaks in KSP
To fix the errors, you must understand why they happen. KAPT and KSP operate at different stages of the compilation pipeline.
1. The Java Stub Barrier
KAPT works by generating Java stubs from your Kotlin code so that traditional Java annotation processors (like Dagger or Room) can read them. This stub generation is expensive and causes a significant build bottleneck.
KSP bypasses this entirely. It parses Kotlin code directly. However, this means KSP relies on a different set of internal APIs to generate code. If a library (like Hilt) expects a specific Java-centric folder structure or argument format that KAPT provided, KSP will fail to generate the necessary files, leading to "Unresolved Reference" errors.
2. Source Set Isolation
KSP is stricter about source sets. When KAPT generates files, they are often dumped into a directory that the IDE blindly indexes. KSP generates files into build/generated/ksp/, and while the Google plugin handles the source set registration, misconfigurations in Gradle (specifically regarding processor arguments) result in zero files being generated. The compiler cannot reference a file that was never created.
Step-by-Step Fix: The Migration Protocol
The following solution assumes a modern Android project using Gradle Version Catalogs (libs.versions.toml) and Kotlin DSL (build.gradle.kts).
Step 1: Aligning Versions (The Pre-requisite)
KSP versions are tied strictly to your Kotlin version. A mismatch here is the #1 cause of silent failures.
File: gradle/libs.versions.toml
[versions]
# Ensure the KSP version matches your Kotlin version exactly
# Format: <kotlin-version>-<ksp-release-number>
kotlin = "1.9.22"
ksp = "1.9.22-1.0.17"
hilt = "2.51"
room = "2.6.1"
[libraries]
# Room
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
Step 2: Plugin Application
Remove kotlin-kapt and apply com.google.devtools.ksp.
File: app/build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
// REMOVE: alias(libs.plugins.kotlin.kapt)
// ADD:
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
Step 3: Dependency Swap & Argument Refactoring
This is where the "Unresolved Reference" errors are fixed. You cannot simply replace kapt with ksp in the dependencies block; you must also migrate the processor arguments.
KAPT passes arguments via kapt { arguments { ... } }. KSP uses a dedicated ksp { arg(...) } block. If you leave the arguments in the KAPT block, Room will not know where to export schemas, and Hilt may fail to aggregate modules.
File: app/build.gradle.kts
android {
// ... namespace, compileSdk, etc.
defaultConfig {
// ...
// REMOVE: javaCompileOptions.annotationProcessorOptions related to Room
}
// REMOVE:
// kapt {
// correctErrorTypes = true
// arguments {
// arg("room.schemaLocation", "$projectDir/schemas")
// }
// }
// ADD: KSP Configuration
ksp {
// Fix for Hilt: Allow KSP to resolve types correctly
arg("dagger.fastInit", "enabled")
arg("dagger.hilt.android.internal.disableAndroidSuperclassValidation", "true")
// Fix for Room: Correctly pass the schema location
arg("room.schemaLocation", "$projectDir/schemas")
}
}
dependencies {
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
// CHANGE: kapt to ksp
ksp(libs.androidx.room.compiler)
// Hilt
implementation(libs.hilt.android)
// CHANGE: kapt to ksp
ksp(libs.hilt.compiler)
}
Step 4: Clean and Invalidate
KSP and KAPT cache mechanisms conflict. After changing the build files:
- Run
./gradlew clean. - In Android Studio: File > Invalidate Caches / Restart.
- Sync Gradle.
- Build Project.
Deep Dive: Handling Generated Sources
A common issue immediately following migration is the IDE marking generated code (like AutoValue classes or DaggerAppComponent) as red, even if the build succeeds.
The IDE Indexing Latency
KSP generates sources into: build/generated/ksp/debug/java and build/generated/ksp/debug/kotlin.
Usually, the KSP plugin automatically registers these paths as source sets. However, in complex multi-module projects, the IDE might lag. If you encounter this, explicitly add the KSP build directory to your source sets in build.gradle.kts:
android {
// ...
sourceSets {
getByName("debug") {
java.srcDir("build/generated/ksp/debug/java")
kotlin.srcDir("build/generated/ksp/debug/kotlin")
}
getByName("release") {
java.srcDir("build/generated/ksp/release/java")
kotlin.srcDir("build/generated/ksp/release/kotlin")
}
}
}
Note: Only do this if the standard plugin application fails to register the sources automatically.
Common Pitfalls and Edge Cases
1. The "Hybrid" Mode Trap
You can theoretically run KAPT and KSP side-by-side. Avoid this. If you keep id("kotlin-kapt") applied while using KSP for Room, you double the build overhead (generating stubs for KAPT + parsing for KSP). Migrate all processors (Moshi, Glide, Room, Hilt) simultaneously for the performance benefit.
2. Room Schema Verification Failures
If you use the room.schemaLocation argument, ensure the directory exists. KAPT would sometimes create the folder for you; KSP is often stricter. If you see an error about writing the schema, manually create the schemas folder in your project root.
3. Moshi Codegen
If you use Moshi for JSON parsing, ensure you migrate moshi-kotlin-codegen.
// build.gradle.kts
dependencies {
implementation(libs.moshi.kotlin)
ksp(libs.moshi.kotlin.codegen)
}
If you forget this, Moshi will fall back to reflection at runtime, which works but significantly degrades app startup performance.
4. Hilt and "Correct Error Types"
In KAPT, we often used kapt { correctErrorTypes = true } to make Hilt errors readable. In KSP, this behavior is generally default, but if you encounter bizarre type-inference errors involving Generics in ViewModels, ensure you are using Hilt version 2.48 or higher, which has mature KSP support.
Conclusion
Migrating to KSP eliminates the Java stub generation step, resulting in build times that are often 30-50% faster for clean builds and significantly faster for incremental builds.
The key to fixing "Unresolved Reference" errors lies in the ksp { arg(...) } block. By correctly passing schema locations and compiler flags through the KSP DSL rather than the deprecated KAPT closure, you ensure the symbol processor has the data required to generate the underlying boilerplate your application relies on.