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

Kotlin
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:

Java
@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

Kotlin
@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:

Kotlin
/**
 * 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:

Kotlin
@Suppress class ExampleClass {}

The decompiled Kotlin bytecode will not contain any reference to “suppress”

⇒ RUNTIME (Default)

Kotlin
@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

Kotlin
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class IoDispatcher

⇒ BINARY

Kotlin
@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.

Kotlin
@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

Kotlin
@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.

Kotlin
@MustBeDocumented
annotation class HelloAnnotation

Annotations Constructor

Kotlin annotations can have a constructor which requires parameters. These parameters can take the following types:

  1. Java primitive types
  2. Annotation classes
  3. Enums
  4. Strings
  5. KClass
  6. Arrays of all the types above
Kotlin
@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:

Kotlin
@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.

Kotlin
// 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:

Kotlin
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.

Kotlin
@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

Kotlin
class HelloClass(@property:Example var p1: Int) {}

The decompiled bytecode looks something like this:

Kotlin
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

Kotlin
class HelloClass(@field:Example var p1: Int) {}

The decompiled bytecode looks something like this:

Kotlin
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.

Kotlin
class HelloClass(@param:Example var p1: Int) {}

The decompiled bytecode looks something like this:

Kotlin
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.

Kotlin
class HelloClass(var p1: Int) {}

fun @receiver:Example HelloClass.sayHello() {
    this.p1 = 0
    println("Hello $this")
}

@setparam

@setparam annotates the property setter parameter.

Kotlin
class HelloClass(@setparam:Example var p1: Int) {}

The decompiled bytecode looks something like this:

Kotlin
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:

Kotlin
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.

Kotlin
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:

  1. If you use a Java annotation with multiple parameters, use the named argument syntax in Kotlin.
  2. If the Java annotation has the value parameter, you need to specify it in Kotlin.
  3. If the value parameter is an array type, it becomes a vararg parameter in Kotlin.
  4. 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:

  1. Annotation Processing: Supercharge Your Development
  2. Advanced Annotation Processing
  3. 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!