Write a Symbol Processor with Kotlin Symbol Processing
Learn how to get rid of the boilerplate code within your app by using Kotlin Symbol Processor (KSP) to generate a class for creating Fragments By Anvith Bhat.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Write a Symbol Processor with Kotlin Symbol Processing
25 mins
- Getting Started
- Project Structure
- Exploring Code Generating Techniques
- KAPT
- Kotlin Compiler Plugin
- KSP
- Getting Familiar with Annotations
- Defining your own Annotation
- Annotating the Fragment
- Addressing Symbol Processors
- Adding a Processor
- Defining a Provider
- Registering the Provider
- Processing Annotations
- Filtering Annotations
- Validating Symbols
- Adding Hierarchy Checks
- Validating Annotation Data
- Using the Validator
- Generating the Fragment Factory
- Creating a Visitor
- Using KotlinPoet for Code Generation
- Creating the Factory Generator
- Generating the Factory Code
- Updating the Visitor to Generate Code
- Integrating Processed Code
- Where to Go From Here?
Kotlin Symbol Processing (KSP) enables you to add code generation capability across your app. KSP leverages annotations to build lightweight compiler plug-ins. You can use it to generate boilerplate or add powerful functionalities to your code.
This topic also requires basic knowledge of annotations. If you’re not familiar with this topic, read Annotations: Supercharge Your Development tutorial first.
This topic also requires basic knowledge of annotations. If you’re not familiar with this topic, read Annotations: Supercharge Your Development tutorial first.
In this tutorial, you’ll build a symbol processor that generates a factory class for Fragment. The factory class lets you pass data to the Fragment via a Bundle during instantiation.
You’ll also learn other details:
- Configuring an annotation.
- How KSP views your code.
- Passing arguments to the processor and logging traces.
Getting Started
Click Download Materials at the top or bottom of this tutorial and then open the starter project in Android Studio.
Project Structure
You’ll now notice three modules:
- Annotation: This holds your annotation class.
- Processor: This contains KSP code generation logic.
- App: This is the Android app that consumes the generated files.
These modules are preconfigured with dependencies. The overall dependency structure is as shown below.
Both :app and :processor module include the :annotation module. Furthermore, the :app module also depends on the :processor module.
Build and run.
The image above is the ShuffleFragment
. Clicking the button selects a random pokemon.
This is the DetailFragment
that shows the details of the pokemon. The shuffled pokemon is passed to the DetailFragment
via a Bundle
. Our aim is to replace createDetailFragment
function in DetailFragment
with a generated one.
Before you jump into updating the project, it’s helpful to understand what KSP and code generation is about.
Exploring Code Generating Techniques
You can generate code for Kotlin sources in three ways:
KAPT
Kotlin Annotation Processing Tool — or kapt
— is a code generation solution that makes Java’s annotationProcessor
work for Java and Kotlin files. While it’s easy to transition to, it relies on extracting Java entities from Kotlin source files that the processor can understand. This makes it slower for Kotlin files.
Kotlin Compiler Plugin
Kotlin Compiler Plugins are modules that have access to low-level APIs of the Kotlin compiler. Most frequently, they’re used to generate code; however, they can also modify existing bytecode and provide richer functionalities to existing code.
A good example is the Parcelize plug-in that generates Parcelable implementations for data classes. This approach has a few drawbacks, including the compiler APIs changing frequently and maintenance getting difficult. Additionally, these compiler APIs aren’t documented well, so working with them gets harder.
KSP
KSP tries to bridge the gap between writing compiler plug-ins and maintainability. Think of it as a layer protecting your code generator from compiler API changes. That also means some functionality of compiler plug-ins would not be available. KSP is Kotlin First, which means it recognizes Kotlin syntax. That makes it faster because it doesn’t rely on extracting Java entities.
Now that you know more about KSP, let’s start by updating the project with annotations.
Getting Familiar with Annotations
Annotations are the entry points within your source code. Most code generation tools rely on it. KSP works on the same foundation.
Defining your own Annotation
You’ll begin by defining an annotation class. That serves as a way to look up Fragments that need a factory. Head to the :annotation module in your project and add a new file called FragmentFactory.kt.
Next, add the annotation declaration to it:
package com.yourcompany.fragmentfactory.annotation
import kotlin.annotation.AnnotationRetention.SOURCE
import kotlin.reflect.KClass
@Target(AnnotationTarget.CLASS) //1
@Retention(SOURCE) //2
annotation class FragmentFactory(val type: KClass<*>, val parcelKey: String)//3
Let’s go over this step by step:
-
This declaration indicates your annotation should be used on classes. Because you’re interested in Fragments only, this would be the correct choice. Other options are on a class
PROPERTY
and aFUNCTION
. -
SOURCE
value for retention means you wish FragmentFactory to be available only at compilation time and not within your APK. -
The
type
parameter provides you the class type of the object that needs to be parcelled. TheparcelKey
is the key that would be used for storing the serialized data in the Bundle.
Annotating the Fragment
Open DetailFragment and add the content below just above the fragment declaration.
import com.yourcompany.fragmentfactory.DetailFragment.Companion.KEY_POKEMON
import com.yourcompany.fragmentfactory.annotation.FragmentFactory
@FragmentFactory(Pokemon::class,KEY_POKEMON)
This makes the DetailFragment discoverable to your processor via the annotation. You’re now ready to write the processor that reads this annotation.
Addressing Symbol Processors
KSP invokes Symbol Processors during the compilation phase. All the logic of filtering KSP tokens and code generation happens within them.
Adding a Processor
You’ll start by navigating to the package com.yourcompany.fragmentfactory.processor
in the :processor module of the project. Next, create a FragmentFactoryProcessor.kt file in it.
Follow up by adding the processor declarations to it.
package com.yourcompany.fragmentfactory.processor
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.KSAnnotated
class FragmentFactoryProcessor(
private val logger: KSPLogger,
codeGenerator: CodeGenerator
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
logger.info("FragmentFactoryProcessor was invoked.")
return emptyList()
}
}
Your symbol processor will extend the SymbolProcessor class and implement a process
method. Kotlin symbol processing happens in multiple rounds. In each round, process()
can return a list of symbols that aren’t available or will be processed in future rounds. This is called deferred processing and enables multiple processors to play well with each other when one is dependent on the output of another.
Defining a Provider
In KSP, Provider is just a factory of your processor. You’ll generally return an instance of your processor here. Go ahead and add a new file called FragmentFactoryProcessorProvider.kt under the existing package com.yourcompany.fragmentfactory.processor
. This will reside in the :processor module as well.
Next, add the following code to it:
package com.yourcompany.fragmentfactory.processor
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
class FragmentFactoryProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return FragmentFactoryProcessor(
logger = environment.logger,
codeGenerator = environment.codeGenerator
)
}
}
The create
function is invoked whenever KSP needs to create an instance of your SymbolProcessor. This gives you access to the environment
which provides the default logger. The codeGenerator
provides methods for creating and managing files. Furthermore, only the files that are created from it are available to KSP for incremental processing and compilations.