Applying Annotations

First, though, let’s explore how we can annotate our Kotlin code, using annotations that somebody else created.

Basic Syntax

For simple use of annotations, the syntax is nearly identical to that of Java. For example, here is the Kotlin equivalent to the JUnit 4 example shown above:

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
  @Test
  fun useAppContext() {
    val appContext = InstrumentationRegistry.getTargetContext()
    assertEquals("com.commonsware.jetpack.hello", appContext.packageName)
  }
}
You can learn more about JUnit4 in Android in the "Touring the Tests" chapter of Elements of Android Jetpack!

As with Java, Kotlin annotations are prefixed with @ and are used to decorate classes, functions, properties, etc. Some annotations take parameters, such as the @RunWith annotation — we will examine this more closely later in this chapter.

And, as with Java, whitespace does not matter. An annotation does not have to appear on a separate line from what it annotates. The above example could be rewritten as:

@RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest {
  @Test fun useAppContext() {
    val appContext = InstrumentationRegistry.getTargetContext()
    assertEquals("com.commonsware.jetpack.hello", appContext.packageName)
  }
}

Specialized Scenarios

Annotating classes, functions, and properties is fairly straightforward: just put the annotation immediately before the element that it is annotating.

However, there are a few things that can be annotated that may not be quite as obvious.

Constructors

Most Kotlin classes have a single constructor, written with the constructor parameter list immediately after the class name:

class Foo(var count: Int) {
  fun something() {
    count += 1
    println("something() was called $count times")
  }
}

As we saw back in the chapter on classes, this is a shorthand replacement for the formal approach:

class Foo constructor(var count: Int) {
  fun something() {
    count += 1
    println("something() was called $count times")
  }
}

If you need to annotate a constructor, you will need to use the formal approach, so you can place the annotation immediately before the constructor keyword:

class Foo @MyAnnotation constructor(var count: Int) {
  fun something() {
    count += 1
    println("something() was called $count times")
  }
}

Lambda Expressions

It is possible to annotate a lambda expression, by putting the annotation immediately before the opening brace:

val lambdaLambdaLambda = @MyAnnotation { doSomethingHere() }

Under the covers, a lambda expression “expands” into an instance of a class with a single invoke() function, with the body of the lambda expression forming the body of that invoke() function. Annotating the lambda expression, in effect, annotates that invoke() function.

Annotation Parameters

Some annotations take parameters, akin to a class constructor. The syntax is similar to class constructors with some minor differences. However, most of the time, they will look akin to how annotations appear in Java. So, for example, the simplest form of using the @Deprecated annotation just passes in a String that is the explanation for the deprecation:

@Deprecated("do not use this, because reasons")
fun thisSeemsPerfectlyFine() {
  // do something
}

Named Parameters

You do not have to use named parameters with Kotlin annotations. This runs counter to Java, where there are scenarios where you have to name the parameters used in annotations.

However, if you want to use named parameters, you are welcome to do so:

@Deprecated(message = "do not use this, because reasons")
fun thisSeemsPerfectlyFine() {
  // do something
}

Arrays

Sometimes an annotation will take an array of values for a parameter, rather than a single value. For that, you have two options:

@Index(arrayOf("otherId", "yetAnotherId"))
@Index(["otherId", "yetAnotherId"])

Array literal syntax was added to Kotlin in version 1.2, so some older materials might not mention it as an option.

Nested Annotations

It is possible that one of the parameters to an annotation is another annotation. In that case, the nested annotation does not get the @ prefix:

@Deprecated("do not use this, because reasons", ReplaceWith("thisIsMuchBetter()"))
fun thisSeemsPerfectlyFine() {
  // do something
}

This is different than Java’s approach, which retains the @ prefix on nested annotations.

Class References

Sometimes, a parameter to an annotation is a class that is to be used by the annotation processor.

For example, in Android development, Room is a library for mapping Java classes to relational database tables, specifically for Android’s SQLite database. Room has a RoomDatabase class that needs to be annotated with a @Database annotation. That annotation, in turn, takes an array of classes that represent the “entities” (the Java model classes that will have corresponding tables in the database).

For cases like this, we use simple ::class notation to identify the class:

@Database(entities = [SimpleEntity::class], version = 1)
abstract class SimpleDatabase : RoomDatabase() {
  // TODO add in the rest of the stuff that Room needs
}

Here, ::class identifies the Kotlin class (i.e., SimpleEntity::class is the Kotlin SimpleEntity class).

In reality, this annotation needs a Java class, but that is handled by the Kotlin compiler when it processes annotations. We will explore the difference between Kotlin class objects and Java class objects later in the book.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.