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.

5 (2) · 1 Review

Download materials
Save for later
Share

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.

Note: This tutorial assumes you’re familiar with Android development and Android Studio. If not, go to Beginning Android Development and Kotlin for Android.

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

Built Starter Project

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.

Annotation-Processor-App dependencies

Both :app and :processor module include the :annotation module. Furthermore, the :app module also depends on the :processor module.

Build and run.

Shuffle Screen

The image above is the ShuffleFragment. Clicking the button selects a random pokemon.

Detail Screen

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.

add ksp annotation file
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:

  1. 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 a FUNCTION.
  2. SOURCE value for retention means you wish FragmentFactory to be available only at compilation time and not within your APK.
  3. The type parameter provides you the class type of the object that needs to be parcelled. The parcelKey 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.

add fragment factory processor

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.

Add Fragment Factory provider

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.