How to Apply Annotations in Kotlin: Best Practices & Examples

Table of Contents

Annotations are a powerful feature in Kotlin that let you add metadata to your code. Whether you’re working with frameworks like Spring, Dagger, or Jetpack Compose, or building your own tools, knowing how to apply annotations in Kotlin can drastically improve your code’s readability, structure, and behavior.

In this guide, we’ll walk through everything step by step, using real examples to show how annotations work in Kotlin. You’ll see how to use them effectively, with clean code and clear explanations along the way..

What Are Annotations in Kotlin?

Annotations are like sticky notes for the compiler. They don’t directly change the logic of your code but tell tools (like compilers, IDEs, and libraries) how to handle certain elements.

If you use @JvmStatic, Kotlin will generate a static method that Java can call without needing to create an object. It helps bridge Kotlin and Java more smoothly.?

Kotlin
object Utils {
    @JvmStatic
    fun printMessage(msg: String) {
        println(msg)
    }
}

This makes printMessage() callable from Java without creating an instance of Utils.

How to Apply Annotations in Kotlin

To apply an annotation in Kotlin, you use the @ symbol followed by the annotation’s name at the beginning of the declaration you want to annotate. You can apply annotations to functions, classes, and other code elements. Let’s see some examples:

Here’s an example using the JUnit framework, where a test method is marked with the @Test annotation:

Kotlin
import org.junit.*

class MyTest {
    @Test
    fun testTrue() {
        Assert.assertTrue(true)
    }
}

In Kotlin, annotations can have parameters. Let’s take a look at the @Deprecated annotation as a more interesting example. It has a replaceWith parameter, which allows you to provide a replacement pattern to facilitate a smooth transition to a new version of the API. The following code demonstrates the usage of annotation arguments, including a deprecation message and a replacement pattern:

Kotlin
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }

In this case, when someone uses the remove function in their code, the IDE will not only show a suggestion to use removeAt instead, but it will also offer a quick fix to automatically replace the remove function with removeAt. This makes it easier to update your code and follow the recommended practices.

Annotations in Kotlin can have arguments of specific types, such as primitive types, strings, enums, class references, other annotation classes, and arrays of these types. The syntax for specifying annotation arguments is slightly different from Java:

To specify a class as an annotation argument, use the ::class syntax:

When you want to specify a class as an argument for an annotation, you can use the ::class syntax.

Kotlin
@MyAnnotation(MyClass::class)

In this case, let’s say you have a custom annotation called @MyAnnotation, and you want to pass a class called MyClass as an argument to that annotation. In this case, you can use the ::class syntax like this: @MyAnnotation(MyClass::class).

By using ::class, you are referring to the class itself as an object. It allows you to pass the class reference as an argument to the annotation, indicating which class the annotation is associated with.

To specify another annotation as an argument, don’t use the @ character before the annotation name:

when specifying an annotation as an argument for another annotation, you don’t need to use the “@” symbol before the annotation name.

Kotlin
@Deprecated(replaceWith = ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }

In the above example, the @Deprecated annotation. It allows you to provide a replacement pattern using the ReplaceWith annotation. In this case, you simply specify the ReplaceWith annotation without the “@” symbol when using it as an argument for @Deprecated .

By omitting the “@” symbol, you indicate that the argument is another annotation.

To specify an array as an argument, use the arrayOf function:

if you want to specify an array as an argument for an annotation, you can use the arrayOf function.

For example, let’s say you have an annotation called @RequestMapping with a parameter called path, and you want to pass an array of strings ["/foo", "/bar"] as the value for that parameter. In this case, you can use the arrayOf function like this:

Kotlin
@RequestMapping(path = arrayOf("/foo", "/bar"))

However, if the annotation class is declared in Java, you don’t need to use the arrayOf function. In Java, the parameter named value in the annotation is automatically converted to a vararg parameter if necessary. This means you can directly provide the values without using the arrayOf function.

To use a property as an annotation argument, you need to mark it with a const modifier:

In Kotlin, annotation arguments need to be known at compile time, which means you cannot refer to arbitrary properties as arguments. However, you can use the const modifier to mark a property as a compile-time constant, allowing you to use it as an annotation argument.

To use a property as an annotation argument, follow these steps:

  1. Declare the property using the const modifier at the top level of a file or inside an object.
  2. Initialize the property with a value of a primitive type or a String.

Here’s an example using JUnit’s @Test annotation that specifies a timeout for a test:

Kotlin
const val TEST_TIMEOUT = 100L

@Test(timeout = TEST_TIMEOUT)
fun testMethod() {
    // Test code goes here
}

In this example, TEST_TIMEOUT is declared as a const property with a value of 100L. The timeout parameter of the @Test annotation is then set to the value of TEST_TIMEOUT. This allows you to specify the timeout value as a constant that can be reused and easily changed if needed.

Remember that properties marked with const need to be declared at the top level of a file or inside an object, and they must be initialized with values of primitive types or String. Using regular properties without the const modifier will result in a compilation error with the message “Only ‘const val’ can be used in constant expressions.”

Best Practices for Using Annotations

Using annotations the right way keeps your Kotlin code clean and powerful. Here are some tips:

1. Use Target and Retention Wisely

  • @Target specifies where your annotation can be applied: classes, functions, properties, etc.
  • @Retention controls how long the annotation is kept: source code only, compiled classes, or runtime.
Kotlin
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogExecutionTime

Use RUNTIME if your annotation will be read by reflection.

2. Keep Annotations Lightweight

Avoid stuffing annotations with too many parameters. Use defaults whenever possible to reduce clutter.

Kotlin
annotation class Audit(val user: String = "system")

3. Document Custom Annotations

Treat annotations like part of your public API. Always include comments and KDoc.

Kotlin
/**
 * Indicates that the method execution time should be logged.
 */
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogExecutionTime

Example: Logging Execution Time

Let’s say you want to log how long your functions take to execute. You can create a custom annotation and use reflection to handle it.

Kotlin
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogExecutionTime

class Worker {
    @LogExecutionTime
    fun doWork() {
        Thread.sleep(1000)
        println("Work done!")
    }
}

Now add logic to read the annotation:

Kotlin
fun runWithLogging(obj: Any) {
    obj::class.members.forEach { member ->
        if (member.annotations.any { it is LogExecutionTime }) {
            val start = System.currentTimeMillis()
            (member as? KFunction<*>)?.call(obj)
            val end = System.currentTimeMillis()
            println("Execution time: ${end - start} ms")
        }
    }
}

fun main() {
    val worker = Worker()
    runWithLogging(worker)
}

This will automatically time any function marked with @LogExecutionTime. Clean and effective.

Advanced Use Case: Dependency Injection with Dagger

In Dagger or Hilt, annotations are essential. You don’t write much logic yourself; instead, annotations do the work.

Kotlin
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    fun provideApiService(): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.softaai.com")
            .build()
            .create(ApiService::class.java)
    }
}

Here, @Module, @Provides, and @InstallIn drive the dependency injection system. Once you learn how to apply annotations in Kotlin, libraries like Dagger become far less intimidating.

Conclusion

Annotations in Kotlin are more than decoration — they’re metadata with a purpose. Whether you’re customizing behavior, interfacing with Java, or using advanced frameworks, knowing how to apply annotations in Kotlin gives you a real edge.

Quick Recap:

  • Use annotations to add metadata.
  • Apply built-in annotations to boost interoperability and performance.
  • Create your own annotations for clean, reusable logic.
  • Follow best practices: target, retention, defaults, and documentation.

With the right approach, annotations make your Kotlin code smarter, cleaner, and easier to scale.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!