Annotations in Programming with Kotlin ft. KSP
Annotations in Programming with Kotlin ft. KSP
Like most things in life, we can’t define Annotation in just one way. Here are a few ways I think of an annotation:
- छोटा पैकेट, बड़ा धमाका: This translates to something that looks small but is very powerful or impactful.
- An annotation is extra or other information associated with a particular point in a document wikipedia.
- Annotations are like a
tag
given to something. It can have some logic (literally!) or be used just for reference. For example, you might call your friend Sam agenius
, which implies they are very knowledgeable about something. Here, there’s some logic behind the label. But you could also call your friend SamSammy
just for ease of use, which is purely a reference without additional logic. - So, they are like a
data about a data
i.e.Metadata
.
Now, we have some knowledge about it. Lets learn about it’s use in Kotlin:
To create an annotation in Kotlin, you define a class with the annotation
keyword:
1
annotation class Genius
Now, you can use it anywhere. For example:
1
2
3
4
fun main() {
val jim = Student(name = "Jim")
@Genius val tim = Student(name = "Tim")
}
Here, our intention is to annotate the variable user2 with Genius. However, the problem is that we can apply this annotation to anything, like a class, function, or even an expression:
1
2
3
4
5
6
@Genius
class Result {
@Genius fun getMarks(): Int {
// Implementation here
}
}
This might not be what we want, so Kotlin provides a way to specify where annotations are allowed to be used. You can declare the targets for the annotation like this:
1
2
@Target(AnnotationTarget.PROPERTY)
annotation class Genius
This ensures that the Genius
annotation can only be applied to properties. If you try to use it elsewhere, the compiler will throw an error.
We can set annotation’s target type (e.g., class, function, property) and retention policy (e.g., runtime, binary, source). Check all available types here: AnnotationTarget and AnnotationRetention
You might think what are this annotations(@Target, @Retention, @Repeatable, @MustBeDocumented) right?
These are called Meta-Annotations.
What are Meta-Annotations?
Meta-Annotations are annotations about annotations
. Just like annotations themselves are data about data
. Cool, right?
We can also create our own Meta-Annotations.
Here’s how we create Meta-Annotation:
1
2
3
@Target(AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class MetaGenius(val description: String = "Meta-Annotation for Genius")
Use it with new annotation @Genius
1
2
@MetaGenius(description = "This annotation marks a class or function as genius.")
annotation class GeniusClass
Ok, But what’s the use of doing all of these?
Processing annotations using KSP
Now that we know what annotations and meta-annotations are, let’s utilize their power by finding all the geniuses in our class:
Set up KSP / Kotlin Symbol Processing for this:
Here’s a summary of setting up KSP:
- Add Dependencies
- Create Annotations
- Implement SymbolProcessor and SymbolProcessorProvider interfaces
- Register your processor provider
- Make the IDE aware of generated code
Here’s how your processor and processor provider might look:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import kotlin.reflect.KClass
class GeniusProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
// Find all symbols annotated with @Genius
val symbols = resolver.getSymbolsWithAnnotation(Genius::class.qualifiedName!!)
.filterIsInstance<KSPropertyDeclaration>()
// Iterate through all annotated properties
symbols.forEach { symbol ->
val studentName = symbol.simpleName.asString()
val className = symbol.parentDeclaration?.qualifiedName?.asString()
logger.warn("Found Genius student: $studentName in class $className")
}
return emptyList()
}
}
class GeniusProcessorProvider : SymbolProcessorProvider {
override fun create(
environment: SymbolProcessorEnvironment
): SymbolProcessor {
return GeniusProcessor(environment.codeGenerator, environment.logger)
}
}
Use it in main fun:
1
2
3
4
5
6
class TestingKSP {
fun main() {
val jim = Student(name = "Jim")
@Genius val tim = Student(name = "Tim")
}
}
After compiling, if everything is set up correctly, the processor will log:
1
Found Genius student: tim in class TestingKSP
This is just a basic example. We can do much more using annotations in our code.
Now can we say that annotations are frontend of the complicated process running behind the scene and making life easy for user(here, it is developer)?
don’t hesitate to use them wisely 😉
checkout KotlinPoet for code generation and use it with ksp to generate code.
Follow official documentation for latest guidelines: