Annotations are an essential part of the Kotlin language, providing a way to add metadata to classes, methods, properties, and others. Mastering the details of annotations is critical to unlocking the full potential of Kotlin, making it easier to document and write more efficient and readable code.
In this blog post, we will dive deep to understand how annotations work in Kotlin.
Why Annotations?
Kotlin annotations are used to attach metadata to code. This allows the compiler or the reflection API to access this metadata for various purposes. For example, the compiler can use annotations to check for errors in the code, generate code, or to analyze code.
Creating a custom annotation
annotation class HelloAnnotation
That’s all you need to define a custom annotation you can apply on any element.
Here is what the decompiled Kotlin bytecode looks like:
@Retention(RetentionPolicy.RUNTIME)
// Bla
// Bla..
public @interface HelloAnnotation {}
Yes! Annotation types are a form of interface.
Let’s dive into the details…
We can annotate this annotation class 😁 Yes!
Annotation: @Retention
We use the retention annotation to specify whether the annotation will be available for source-processing tools, at runtime, or in the compiled class files. But what is the difference?
⇒ SOURCE
@Retention(AnnotationRetention.SOURCE)
annotation class HelloAnnotation
SOURCE Retention annotations are unavailable for the reflection API and the compiled class files. They are used to provide information to the source-processing tools. To better understand, let’s look at the “Suppress” annotation from the Kotlin Standard Library:
/**
* Suppresses the given compilation warnings in the annotated element.
* @property names names of the compiler diagnostics to suppress.
*/
@Retention(SOURCE)
public annotation class Suppress(vararg val names: String)
We all used it before to suppress compilation warnings by the compiler. It is discarded during the runtime and will not be present in the compiled class files.
Let’s use the suppress annotation:
@Suppress class ExampleClass {}
The decompiled Kotlin bytecode will not contain any reference to “suppress”
⇒ RUNTIME (Default)
@Retention(AnnotationRetention.RUNTIME)
annotation class HelloAnnotation
RUNTIME annotations are available for the reflection API and get discarded during compilation. They are also available in the compiled class files.
Let’s see an example when declaring a qualifier for Dagger
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class IoDispatcher
⇒ BINARY
@Retention(AnnotationRetention.BINARY)
annotation class HelloAnnotation
Similar to RetentionPolicy.CLASS
in Java, BINARY
annotations are only available in the compiled class files. They are used only when an annotation must be present in the compiled files and irrelevant for runtime.
For example, the ProGuard tool operates on “.jar” files, so annotations like @Keep
and @KeepClassMembers
need to be present but irrelevant at the runtime.
Annotation: @Target
The @Target
annotation in Kotlin allows developers to specify the types of elements that can be annotated. This includes classes, methods, properties, and others. With this annotation, developers can assign specific annotations to the elements that meet their requirements.
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class HelloAnnotation
@Target
can accept many targets simultaneously or none if nothing was passed as an argument.
Here are all the possible values:
/** Class, interface or object, annotation class is also included */
CLASS,
/** Annotation class only */
ANNOTATION_CLASS,
/** Generic type parameter */
TYPE_PARAMETER,
/** Property */
PROPERTY,
/** Field, including property's backing field */
FIELD,
/** Local variable */
LOCAL_VARIABLE,
/** Value parameter of a function or a constructor */
VALUE_PARAMETER,
/** Constructor only (primary or secondary) */
CONSTRUCTOR,
/** Function (constructors are not included) */
FUNCTION,
/** Property getter only */
PROPERTY_GETTER,
/** Property setter only */
PROPERTY_SETTER,
/** Type usage */
TYPE,
/** Any expression */
EXPRESSION,
/** File */
FILE,
/** Type alias */
@SinceKotlin("1.1")
TYPEALIAS
Annotation: @Repeatable
Kotlin makes it possible to apply the same annotation to one element multiple times by using the @Repeatable
annotation.
Let’s look at the example below
@Repeatable
annotation class HelloAnnotation
/* USAGE */
@HelloAnnotation
@HelloAnnotation
@HelloAnnotation
class TestClass {}
Annotation: @MustBeDocumented
@MustBeDocumented
indicates that the element is part of the public API, and its documentation must be generated. By utilizing this annotation, developers can ensure that all public API elements are accurately and adequately documented.
@MustBeDocumented
annotation class HelloAnnotation
Annotations Constructor
Kotlin annotations can have a constructor which requires parameters. These parameters can take the following types:
- Java primitive types
- Annotation classes
- Enums
- Strings
- KClass
- Arrays of all the types above
@Retention(AnnotationRetention.RUNTIME)
annotation class HelloAnnotation(val klass: KClass<*>)
@MustBeDocumented
annotation class ByeAnnotation(val number: Long, val annotation: HelloAnnotation) // No need to use @ for other annotations
Usage:
@HelloAnnotation(Unit::class)
@ByeAnnotation(12L, HelloAnnotation(Unit::class))
class TestClass {}
Kotlin enables developers to conveniently invoke the constructor of an annotation class as they would with any regular class, providing a great deal of flexibility.
Use-Site Targets
If you have ever looked at a decompiled Kotlin bytecode, you will notice that multiple java elements are generated for the corresponding Kotlin element.
We need a way to specify which Java elements are affected by the annotation.
Kotlin’s annotation use-site targets provide a way to customize the effects of an annotation when applied to a certain element. These targets let developers customize how the annotation is interpreted and how it affects the code.
// Implementation
annotation class Example
// Usage
class HelloClass(@set:Example var p1: Int, @get:Example val p2: Long) {}
// target: p1 setter and p2 getter
Here is what the decompiled bytecode looks like:
public final class HelloClass {
private int p1;
private final long p2;
public final int getP1() { return this.p1; }
@Example <-- Here
public final void setP1(int var1) { this.p1 = var1;}
@Example <-- here
public final long getP2() { return this.p2; }
public HelloClass(int p1, long p2) {
// bla // bla
}
}
Here is the list of all the targets you can use:
@file
The @file target can be used to add annotations to any Kotlin file in a project.
@file:JvmName("Lambda")
// Specifies the name for the Java class or method which is generated from this element.
package com.example.test
/* Rest of the file */
@Property
class HelloClass(@property:Example var p1: Int) {}
The decompiled bytecode looks something like this:
public final class HelloClass {
private int p1;
@Example
public static void getP1$annotations() {}
public final int getP1() {return this.p1;}
public final void setP1(int var1) { this.p1 = var1; }
public HelloClass(int p1) { this.p1 = p1; }
}
@field
class HelloClass(@field:Example var p1: Int) {}
The decompiled bytecode looks something like this:
public final class HelloClass {
@Example
private int p1;
public final int getP1() {return this.p1;}
public final void setP1(int var1) {this.p1 = var1;}
public HelloClass(int p1) {this.p1 = p1;}
}
@get and @set
Explained above 👆
@param
@param
annotations could be applied only to primary constructor parameters.
class HelloClass(@param:Example var p1: Int) {}
The decompiled bytecode looks something like this:
public final class HelloClass {
private int p1;
public final int getP1() { return this.p1; }
public final void setP1(int var1) { this.p1 = var1; }
public HelloClass(@Example int p1) { this.p1 = p1; }
}
@receiver
@receiver
annotates the receiver parameter of an extension function or property.
class HelloClass(var p1: Int) {}
fun @receiver:Example HelloClass.sayHello() {
this.p1 = 0
println("Hello $this")
}
@setparam
@setparam
annotates the property setter parameter.
class HelloClass(@setparam:Example var p1: Int) {}
The decompiled bytecode looks something like this:
public final class HelloClass {
private int p1;
public final int getP1() {return this.p1;}
public final void setP1(@Example int var1) {
this.p1 = var1;
}
public HelloClass(int p1) { this.p1 = p1; }
}
@delegate
To understand @delegate
target, let’s look at the example below:
annotation class Example
class HelloClass(var p1: Int) {
@delegate:Example
val p2 by lazy { p1 * p1 }
}
If you take look at the decompiled bytecode, the annotation will be placed on the generated private backing property, whose type will be the type of the delegate.
public final class HelloClass {
@Example
@NotNull
private final Lazy p2$delegate;
private int p1;
// bla
// bla
// rest of the class
}
Java Interoperability
Kotlin is designed with Java interoperability in mind, including annotations.
Here are some rules you need to stick to:
- If you use a Java annotation with multiple parameters, use the named argument syntax in Kotlin.
- If the Java annotation has the value parameter, you need to specify it in Kotlin.
- If the value parameter is an array type, it becomes a vararg parameter in Kotlin.
- For any other array parameter, it becomes an array in Kotlin.
Now that you know all about Kotlin annotations and how they work, you can start writing even more powerful applications.
If you want to dive deeper into the subject, I would recommend taking a look at the following articles:
- Annotation Processing: Supercharge Your Development
- Advanced Annotation Processing
- KSP: Fact or kapt?
These resources will guide you in building an annotation processor step-by-step.
I hope this post has been useful and informative. Please share it with your friends and colleagues if you find it helpful. Thank you!