Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
108: Annotation Processing with KSP Revisited
A few years ago, I was working with a dev who was convinced their laptop was dying. Every time they made a tiny change to a data class in our core module, the build took nearly two minutes. They were staring at a progress bar, complaining that the "Kotlin compiler is just slow." When I looked at the build logs, I saw the culprit: KAPT. The project had dozens of annotation processors, and KAPT was spending the vast majority of that time generating Java stubs just so the Java-based annotation processors could understand the Kotlin code. It was a massive overhead for something that felt like it should be native.
That's why we're revisiting KSP. If you've used KAPT, you know it's basically a wrapper. KSP (Kotlin Symbol Processing) is the native alternative. It doesn't generate stubs; it plugs directly into the Kotlin compiler. The result isn't just a marginal improvement—it's often a 2x or 3x speedup in build times for heavily annotated projects. But the way you write a KSP processor is fundamentally different from the javax.annotation.processing API you might be used to.
The Symbol Processor Lifecycle
In KSP, you aren't dealing with "Elements" in the Java sense. You're dealing with KSNodes. The heart of your processor is the SymbolProcessor interface. When the compiler hits your processor, it hands you a Resolver. Think of the resolver as your search engine; it's how you ask the compiler, "Hey, find me every single class that has the @Route annotation."
class RouteProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.myapp.Route")
val unableToProcess = symbols.filterNot { it.validate() }.toList()
symbols.filter { it.validate() }.forEach { symbol ->
if (symbol is KSClassDeclaration) {
generateRouteRegistry(symbol)
}
}
return unableToProcess
}
}
One detail that often trips people up is the return value of process(). You return a list of symbols that you couldn't process in this round. This is crucial for incremental processing. If your processor generates a new class that *also* has an annotation your processor needs to handle, you return that symbol so KSP knows to run another round. If you just return an empty list, you're telling KSP you're done.
Navigating the AST with Visitors
Once you have a KSClassDeclaration, you don't want to be manually looping through properties and checking types with a bunch of if/else blocks. It gets messy fast. Instead, we use the Visitor pattern. By implementing KSVisitor, you can define exactly what happens when the processor encounters a property, a function, or a class.
I usually recommend creating a specific visitor for each generation task. For example, if you're generating a RouteRegistry, your visitor should only care about the class name and its constructor parameters. By isolating the "discovery" logic in a visitor, your main SymbolProcessor stays clean, and you avoid the "pyramid of doom" where you're indented ten levels deep just to find a property's name.
The Nuances of Code Generation
Generating the actual file is where the CodeGenerator comes in. You'll notice it requires a Dependencies object. This is the secret sauce for incremental compilation. If you tell KSP that the file you're generating depends on MyRouteClass.kt, KSP is smart enough to only re-run your processor if that specific file changes. If you pass Aggregating dependencies, KSP knows that any change in the project might affect the output. Use Isolating whenever possible; your build times will thank you.
A quick tip: don't try to write raw strings for your generated code. It's a nightmare to maintain and prone to syntax errors. Use KotlinPoet. It handles imports, indentation, and type naming for you, so you can focus on the logic of the processor rather than whether you forgot a closing curly brace in a string template.
📋 Practical Task
Build a Static Schema Generator for SQLite Tables
Your goal is to create a KSP processor that scans for a custom @Table annotation on classes and a @Column annotation on properties. Instead of the app discovering the schema at runtime via reflection, your processor should generate a TableSchemaRegistry object that contains a map of table names to their corresponding column names.
- Create a
@Table(name: String)and@Column(name: String)annotation. - Implement a
SymbolProcessorthat finds all classes annotated with@Table. - Use a
KSVisitorto extract the table name from the class annotation and the column names from the properties annotated with@Column. - Use
CodeGenerator(and ideally KotlinPoet) to produce a file namedTableSchemaRegistry.kt. - The generated file should look something like:
object TableSchemaRegistry { val tables = mapOf("users" to listOf("id", "username", "email")) } - Ensure you use
Dependencies.isolatingto keep the build incremental.
There are no comments for now.