Skip to content
Kotlin Annotations Explained: Guide for Android Developers

5 Minutes

Kotlin Annotations Explained: Guide for Android Developers

Fix Bugs Faster! Log Collection Made Easy

Get started

How Kotlin Annotations work

Kotlin annotations allow compilers or libraries to understand our code.

These metadata tags don’t directly change code logic, but they help modify how it is interpreted, optimized, or validated.

This simplifies Android development by automating repetitive tasks and ensuring consistent code behavior. It also improves code readability, reduces boilerplate code, and introduces automated checks and generation.

Kotlinn annotations can be applied to classes, functions, properties, parameters, and even entire files. For example, in Android, annotations like @NonNull or @SerializedName help ensure type safety or configure serialization behavior.


Why We Use Kotlin annotations

Kotlin annotations have all kinds of use cases in day-to-day coding. Here are some of the most common:

  • Code validation: Enforcing compile-time rules (e.g., @NonNull).
  • Dependency injection: Used in frameworks like Dagger or Hilt.
  • Serialization: Mapping data between JSON and Kotlin objects.
  • Testing: Simplifying test configuration with @Test.
  • Documentation: Making APIs more descriptive with annotations like @Deprecated.

Really, they’ll improve all aspects of your coding game. Ready to start using them? Let’s dive in.


Built-in Kotlin Annotations

Kotlin provides several built-in annotations that you’ll frequently encounter while developing Android apps.

@Deprecated

This marks a function, class, or property as outdated.

@Deprecated("Use newMethod() instead")
fun oldMethod() {}

The compiler warns you when the deprecated element has been used.

@JvmStatic

This is used in companion objects to make methods callable from Java without needing the instance.

companion object {
    @JvmStatic fun show() = println("Static call from Java")
}

@JvmOverloads

Generates overloaded methods for Java interoperability when using default parameters.

fun greet(name: String = "User") = println("Hello, $name")

@JvmField

Exposes a Kotlin property as a public Java field.

class Config {
    @JvmField val version = "1.0"
}

@Synchronized

Ensures that a function is thread-safe.

@Synchronized fun safeIncrement() { /* thread-safe code */ }


How to Apply Kotlin Annotations

Now we’ve looked at the most common out-of-the-box examples of Kotlin annotations, let’s get to work applying them.

Actually, the application process is very simple—you just place the annotation before the element you want to modify. Like this.

class User(
    @SerializedName("user_name") val name: String,
    @NonNull val email: String
)

You can also use multiple annotations:

@Synchronized
@Test
fun loadUserData() { /* ... */ }

Remember: annotations can apply to classes, functions, properties, parameters, or constructors, depending on the target.


How to declare Kotlin Annotations

Kotlin annotations are generally easier to declare than regular classes, because they’re so focused and limited in scope.

You can create your own annotations using the annotation class keyword.

annotation class LogExecution(val level: String = "INFO")

To annotate a constructor

annotation class Inject
class Service @Inject constructor(private val repo: Repo)

To annotate a property

annotation class FieldName(val name: String)
class User(@FieldName("user_id") val id: Int)

Specific types of annotations

These will likely come up a lot during your day-to-day work as a dev.

@Target

Specifies where the annotation can be used.

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MyAnnotation

@Retention

Defines how long the annotation is retained.

@Retention(AnnotationRetention.RUNTIME)
annotation class RuntimeAnnotation

Use cases include:

Retention TypeAvailabilityUse Case Example
SOURCEDiscarded at compile-timeCode linting
BINARYStored in .class but not runtimeJVM processing
RUNTIMEAvailable during runtimeReflection-based logic

@Repeatable

Allows multiple instances of the same annotation on one element.

@Repeatable
annotation class Permission(val value: String)

@MustBeDocumented

Ensures that an annotation appears in generated API docs.

@MustBeDocumented
annotation class PublicAPI

Nested Declarations in Annotations

Means annotations can contain other annotations or enums.

annotation class Log(val level: Level) {
    enum class Level { INFO, WARN, ERROR }
}


How to process Kotlin Annotations

If you’ve been working with Kotlin for a while, you’ll know that it compiles code differently to Java, and this can create challenges when processing.

Thankfully, annotation processing is pretty flexible, and we can call on the Kotlin Annotation Processing Tool, or KAPT, and its successors (as we’ll show below).

Kotlin annotations can be processed at compile-time or runtime, depending on retention policy.

Compile-time processing

Compile-time processors like KAPT generate code before compilation.

Example:

@AutoGenerated class Mapper { /* generated by KAPT */ }

This is used in libraries like Dagger or Room.

Runtime Processing

At runtime, annotations are accessed through reflection:

val annotation = MyClass::class.annotations.find { it is LogExecution }

This is useful when behavior depends on runtime data, like dynamic logging or validation.

Processing TypeWhen It HappensTypical ToolUse Case
Compile-TimeDuring buildKAPTDependency injection
RuntimeDuring executionReflectionLogging, validation

Kotlin processor tools

As well as KAPT, Kotlin’s standard compile-time processor for generating source code, we can draw on a whole bunch of tools including:

  • KSP (Kotlin Symbol Processing): An alternative to KAPT.
  • Annotation Processing APIs: Used for custom build logic and code generation.

Each tool allows Android developers to extend Kotlin’s functionality and integrate seamlessly with frameworks like Room, Glide, or Hilt.


How to create custom Kotlin annotations

Creating custom annotations lets you automate logic and reduce boilerplate. You can also:

  • Express domain intent.
  • Add safety to your code configuration.
  • Promote consistent coding practices across teams.
  • Create cleaner, more readable APIs.

Here’s an example:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogExecution(val message: String)

You can then process this annotation with reflection or a processor to log method calls automatically.


How to ensure Java-interoperability with annotations

Kotlin is fully interoperable with Java, but annotations require special handling to ensure compatibility.

Using the ‘JVM’ (Java Virtual Machine) prefix, we can make Kotlin behave more like Java at the bytecode level and ensure that Kotlin classes behave predictably when called from Java codebases.

Here are some common ones:

Kotlin AnnotationJava EquivalentPurpose
@JvmStaticstatic methodAllows static access
@JvmOverloadsOverloaded methodsEnables default arguments in Java
@JvmFieldPublic fieldAvoids getters/setters
@Throwsthrows declarationPropagates exceptions to Java

Using these annotations ensures that Kotlin classes behave predictably when called from Java codebases.


Exception handling

Like any aspect of programming, Kotlin annotations can throw up unusual situations and edge cases. When working with annotations that may influence runtime behavior, it’s important to handle potential exceptions gracefully.

Common exceptions include:

  • ClassNotFoundException — when referencing missing annotations.
  • IllegalAccessException — when reflection accesses restricted members.
  • NullPointerException — when annotations assume non-null targets.

Be sure to always wrap reflection logic in try-catch blocks and validate annotation presence before use.

Example:

try {
    val log = method.getAnnotation(LogExecution::class.java)
} catch (e: Exception) {
    println("Annotation error: ${e.message}")
}


Summary

Kotlin annotations are powerful tools for adding metadata, enforcing constraints, and automating behavior in Android apps.

  • They simplify dependency injection, serialization, and validation.
  • You can create custom annotations using annotation class.
  • Processing occurs at compile-time (KAPT/KSP) or runtime (reflection).
  • Java interoperability is maintained via @Jvm* annotations.

Hopefully you’ve now got a good grounding in this aspect of programming, so you can go forward and incorporate into your developer’s toolkit. But if you still have questions, then let’s chat: email our support team and we’ll help you work through your question.

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Anupam Singh

Anupam Singh is a Native Android Developer as well as Hybrid Mobile App Developer. More than 10 years of experience in Developing Mobile Apps. He is a technology blogger. Follow him on Linkedin to stay connected.

Join thousands of developers
and start fixing bugs faster than ever.