Kotlin Symbol Processing. Working with annotations in a new way
Good day to everyone! Anna Zharkova, a leading mobile developer at Usetech, is with you. In February 2021, Google announced the experimental release of Kotlin Symbol Processing technology (compatible with Kotlin from 1.4.30) as a more effective alternative to KAPT (Kotlin Annotation Processing Tool). It immediately attracted the attention of many developers who are thinking about introducing annotations into multi-platform projects, despite the recommendations of the creators not to use it in the product. In September, the first stable release was released, and now it is officially ready to work in combat projects.
In this article, I propose to consider the nuances of working with KSP in both Android and Kotlin Multiplatform applications.
So let’s start with the appointment. Kotlin Symbol Processing is designed to develop lightweight Kotlin compilation plugins and annotation processors. The latter are of interest to us. In fact, annotations are needed in the application in order to simplify the work and save us from unnecessary code.
For example, when we need to analyze the code for a specific purpose and then do some actions. Or remove unnecessary abstraction from the application. It looks much more attractive to add literally 1 command over a specific object/method/types, and instead of writing tons of boilerplate for each case, entrust it to a library that will do everything itself.
Let’s see how the annotation processor works in its mechanics. For example, such as we use in Java code:
The processor then scans all sources for the desired annotations. If such were found, work starts for them. The resulting code is then compiled.
When working with KSP and KAPT, we do not modify the current files, but generate new code that compiles with our sources.
Generating new data with KAPT is a long thing. We all know how long a gradle task can run when our code has the same Dagger.
When processing Kotlin code using the Kotlin Annotation Processing Tool, we need to generate Java Stub, which we already use as compiled Java classes when compiling along with other sources. Due to this intermediate step, the whole process can take quite a long time, especially if we have a multi-module application.
Unlike KAPT, KSP doesn’t generate any Java stubs. The processor works directly with the Kotlin AST (abstract syntax tree), which allows you to generate Kotlin code at once, and immediately the one that we will use in the application. Due to this, working with KSP is faster, much more efficient and cleaner.
Now let’s see how the symbol processor works and how to create your own. To do this, we need to use special interfaces for the provider and processor:
The provider (used to create the processor) must be declared as a resource:
In the resource file itself, we simply indicate the full name of the provider used.
Let’s move on to the structure of the processor itself:
To access the source files and code, a special resolver is used. The resulting code is analyzed using Kotlin reflection mechanisms to obtain information about types and parameters, for example, in a special Visitor implementing KSVisitorVoid:
We can also use our own scanner that works on the same principles. We need to determine what element we are dealing with, what settings and properties the annotation has, what parameters we still need to work with, and collect all this information for the following actions.
You can then generate the code. To do this, we use the KotlinPoet library. It allows you to flexibly prescribe the structure of the generated classes and the method, including property values:
Let’s move on to the practical use of KSP. One of the most effective and expected examples of working with this technology is Dependency Injection. And not only in Android, but also in multi-platform applications. And if in previous releases (alpha and beta) it was possible to use only in applications with JS and JVM/Android targets, then from the September release we can work with Kotlin Native.
As an example, I will use my own Multiplatform-DI library, but let’s start by connecting to an Android application.
Inside the library, we are working with special containers in which resolver entities control the storage of links with type, parameters, and factories for creating type instances in certain areas of action (scopes).
Registration/receipt of types is carried out using manual Dependency Injection:
A lot of extra code and our work. Let’s try to automate using annotations (thanks Koin for the inspiration):
That is, we will annotate the scopes and container and change the code for registration as follows:
We will have significantly less code.
Now we need to create a special module in which we will locate our provider, processor, generator and scanner.
The current version of KSP is 1.5.31–1.0.0. Since the KSP is based on JVM, then we target the module on it. We also connect a dedicated module here with the components to which we will use links to generate code. In this case, di-multiplatform-core. To work with ksp, we use “com.google.devtools.ksp: symbol-processing-api: $ kspVersion.” We also need KotlinPoet:
Now let’s deal with the provider and processor itself. We will use our scanner and code generator:
In the scanner, we use a resolver to analyze and study the data. Focus on the getSymbolsWithAnnotation method:
See the source code for more details.
To generate, we need to register a template using Kotlin Poet. That is, we turn the entire source file with a description of registration into a template, where the necessary types and their parameters will be registered:
Now we collect the project and observe the creation of new elements:
And we get the generated file:
And we connect the received class to our code and use it for its intended purpose:
We start and rejoice: