Skip to Content
Course content

201: Kotlin Symbol Processing Evolution

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're an editor for a prestigious literary journal. You receive a manuscript written in a beautiful, complex dialect of a language—let's call it "Kotlin-ese." However, your only experienced proofreader only speaks standard English (Java). To get the manuscript proofread, you have to spend three days translating the entire book into a clunky, literal English version just so the proofreader can spot the errors. Once they're done, you have to manually map those corrections back to the original dialect. It's a tedious, slow process, and in the translation, you lose all the poetic nuance—the "nullability" and "inline functions" of the original text.

For years, that's exactly how we handled annotation processing in Kotlin using KAPT. KAPT (Kotlin Annotation Processing Tool) was essentially a translator. It generated "Java stubs"—skeleton versions of your Kotlin classes—so that the Java Annotation Processing Tool (APT) could understand them. It worked, but it was a massive drag on build times. I've lost count of how many hours I've spent staring at a "Generating Stubs" progress bar in IntelliJ.

The Heavy Toll of Java Stubs

The problem with KAPT wasn't just the speed; it was the blindness. Because KAPT converted everything to Java stubs, the processor couldn't "see" Kotlin-specific features. If you had a property marked as non-nullable (String) versus nullable (String?), the Java-based processor often saw both as just java.lang.String. You were fighting against the language to make the tools work.

This is where KSP (Kotlin Symbol Processing) comes in. KSP is like finally hiring a proofreader who actually speaks "Kotlin-ese." It doesn't translate your code into Java; it reads the Kotlin source code directly. It understands the AST (Abstract Syntax Tree) of your program without the middleman.

Speaking Kotlin Fluently

When you move from KAPT to KSP, you stop dealing with javax.lang.model elements and start dealing with KSNode and KSClassDeclaration. This is a shift in mindset. Instead of asking "What is the Java equivalent of this class?", you're asking "What is this Kotlin symbol?"

Let's look at a concrete example. Say we want to build a simple tool that finds every class annotated with @JsonSerializable and generates a helper function to turn that class into JSON. In the old KAPT world, you'd be wrestling with mirror elements. In KSP, it looks like this:


// We define a SymbolProcessor that implements the logic
class JsonProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        // We directly ask the resolver for symbols with our annotation
        val symbols = resolver.getSymbolsWithAnnotation("com.example.JsonSerializable")
        
        symbols.filterIsInstance<KSClassDeclaration>().forEach { classDecl ->
            val packageName = classDecl.packageName.asString()
            val className = classDecl.simpleName.asString()
            
            // We can see the exact nullability of properties here!
            val properties = classDecl.getAllProperties()
            
            generateJsonHelper(packageName, className, properties)
        }
        return emptyList()
    }
}

I love this approach because it's lightweight. KSP doesn't need to compile your code to bytecode first; it analyzes the symbols. If you're working on a large multi-module project, the difference in build speed is often night and day. You're no longer paying the "stub tax" on every single compilation.

Navigating the Symbol Tree

The real power of KSP is how you can traverse the code. Since you're dealing with symbols, you can easily jump from a function to its return type, or from a property to the class that owns it. You aren't guessing based on a Java translation; you are interacting with the actual structure of your Kotlin code.

One thing to keep in mind: KSP is primarily for generating new code, not for modifying existing code. You can't reach into a class and add a method to it. Instead, you generate a new file (like an extension function) that adds the desired functionality. It's a cleaner separation of concerns, and it keeps the compiler happy.




📋 Practical Task

Build a @Route Registry Processor

Your task is to implement a basic KSP SymbolProcessor that scans for a custom annotation called @Route. Instead of generating a full file, your processor should identify all classes annotated with @Route and print their fully qualified name and the name of their primary constructor to the build console.

Requirements:

  • Create the @Route annotation.
  • Implement a SymbolProcessor that uses resolver.getSymbolsWithAnnotation.
  • Filter the results to ensure you are only processing KSClassDeclaration symbols.
  • Extract and print the qualifiedName of the class.
  • Locate the primary constructor of that class and print its name (or "No primary constructor found").

This exercise will force you to move beyond simple symbol collection and start navigating the relationships between symbols (Class → Constructor).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.