© Peter Späth 2019
Peter SpäthLearn Kotlin for Android Developmenthttps://doi.org/10.1007/978-1-4842-4467-8_14

14. Adding Hints: Annotations

Peter Späth1 
(1)
Leipzig, Germany
 
Annotations are for adding meta-information to your code. What does that mean? Consider the following classes:
class Adder {
    fun add(a:Double, b:Double) = a + b
}
class Subtractor {
    fun subtract(a:Double, b:Double) = a - b
}
If we have a larger arithmetic calculation project where the various operations get handled by classes like Adder and Subtractor here, we could have something like
val eng = CalculationEngine()
...
eng.registerAdder(Adder::class, "add") eng.registerSubtractor(Subtractor::class, "subtract")
...

for registering the particular low-level operations.

We could, however, follow a different approach where the operators somehow announce their abilities to the framework. They could do this by special documentation tags, as in
/**
 * @Operator: ADDING
 * @Function: add
 */
class Adder {
    fun add(a:Double, b:Double) = a + b
}
/**
 * @Operator: SUBTRACTING
 * @Function: subtract
 */
class Subtractor {
    fun subtract(a:Double, b:Double) = a - b
}

Some parser could then look into the source code to find out which classes and functions are needed for the various operators.

Note

A framework is a collection of classes, interfaces, and singleton objects that provide a scaffolding structure to software. A framework is not an executable program itself, but a software project uses the framework to establish a standardized structure. Different projects using a particular framework thus exhibit a similar structure and if a developer knows one project embedded into a particular framework it will be easier to understand other projects using the same framework.

This method of letting classes announce themselves to a program frequently gets used in a server environment where the program needs to be able to communicate with clients over a network.

There is, however, a problem with this approach. Because the meta-information gets presented from inside the documentation, there is no possibility for the compiler to check the correctness of the tags. Concerning the compiler, the contents of the documentation are completely unimportant, and should be unimportant, because this is what the language specification says.

Annotations in Kotlin

This is where annotations enter the game. They exist exactly for this kind of task: not interfering with the class’s primary responsibilities, but providing meta-information to the program or framework for maintenance or registration purposes. An annotation looks like this:
@AnnotationName
or
@AnnotationName(...)
if there are parameters. A lot of language elements can be marked with such annotations: files, classes, interfaces, singleton objects, functions, properties, lambdas, statements, and even other annotations. The operator classes for the preceding calculation engine example could read
@Operator(ADDING)
class Adder {
    @OperatorFunction
    fun add(a:Double, b:Double) = a + b
}
@Operator(SUBTRACTING)
class Subtractor {
    @OperatorFunction
    fun subtract(a:Double, b:Double) = a - b
}

Now the compiler is in a better situation. Because annotations are part of the language the compiler can check whether they exist, are spelled correctly, and have the correct parameters provided.

In the following sections we first discuss annotation characteristics, then annotations that Kotlin provides. We then cover how to build and use our own annotations.

Annotation Characteristics

Annotations get declared by annotation classes as follows:
annotation class AnnotationName
We cover building our own annotations in a later section. For now we mention the declaration because annotations have their characteristics described by their own annotations, which then are meta-annotations:
@Target(...)
@Retention(...)
@Repeatable
@MustBeDocumented
annotation class AnnotationName
You can use any combination of them in any order, and they have default values if unspecified. We describe them, including possible parameters, here.
  • @Target(...)

    Here you specify the possible element types to which the annotation can be applied. The parameter is a comma-separated list of any of the following (all of them are fields of the enumeration kotlin.annotation.AnnotationTarget):
    • CLASS: All classes, interfaces, singleton objects and annotation classes.

    • ANNOTATION_CLASS: Only annotation classes.

    • PROPERTY: Properties.

    • FIELD: A field that is the data holder for a property. Note that a property by virtue of getters and setters does not necessarily need a field. However, if there is a field, this annotation target points to that field. You put it in front of a property declaration, together with the PROPERTY target.

    • LOCAL_VARIABLE: Any local variable (val or var inside a function).

    • VALUE_PARAMETER: A function or constructor parameter.

    • CONSTRUCTOR: Primary or secondary constructor. If you want to annotate a primary constructor, you must use the notation with the constructor keyword added; for example, class Xyz @MyAnnot constructor(val p1:Int, ...).

    • FUNCTION: Functions (not including constructors).

    • PROPERTY_GETTER: Property getters.

    • PROPERTY_SETTER: Property setters.

    • TYPE: Annotations for types, as in val x: @MyAnnot Int = ...

    • EXPRESSION: Statements (must contain an expression).

    • FILE: File annotation. You must specify this before the package declaration and in addition add a file: between the @ and the annotation name, as in @file:AnnotationName.

    • TYPE_ALIAS: We didn’t talk about type aliases yet. They are just new names for types, as in typealias ABC = SomeClass<Int>. This annotation type is for such typealias declarations .

    If unspecified, targets are CLASS, PROPERTY, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, and PROPERTY_SETTER.

  • @Retention(...)

    This specifies where the annotation information goes during compilation and whether it is visible using one of the following (all are fields from the enumeration class kotlin.annotation.AnnotationRetention):
    • SOURCE: Annotation exists only in the sources; the compiler removes it.

    • BINARY: Annotation exists in the compiled classes, interfaces, or singleton objects. It is not possible to query annotations at runtime using reflection.

    • RUNTIME: Annotation exists in the compiled classes, interfaces, or singleton objects, and it is not possible to query annotations at runtime using reflection.

    The default is RUNTIME.

  • @Repeatable

    Add this if you want to allow the annotation to appear more often than just once.

  • @MustBeDocumented

    Add this if you want the annotation to show up in the public API documentation.

You can see that for classes, interfaces, singleton objects, properties, and local properties, you don’t have to specify special characteristics if you want the annotations to show up visibly in the compiled files.

Applying Annotations

In general, annotations get written in front of the element to which the annotation is to apply. The story gets a little bit complicated because it is not always clear what is meant by element. Consider this example:
class Xyz {
    @MyAnnot var d1:Double = 1.0
}
Here we have four elements to which the annotation could be applied: the property, the property getter, the property setter, and the data field. For this reason, Kotlin introduced use-site targets in the form of a qualifier: written between the @ and the annotation name. The following use-site targets are available:
  • file

    We know that a Kotlin file can contain properties and functions outside classes, interfaces, and singleton objects. For an annotation applying to such a file, you write @file:AnnotationName in front of the package declaration. For example:
    @file:JvmName("Foo")
    package com.xyz.project
    ...

    to give the internally created class the name Foo.

  • property

    The annotation gets associated with the property. Note that if you use Java to access your Kotlin classes, this annotation is not visible to Java.

  • field

    The annotation gets associated with the data field behind a property.

  • get

    The annotation gets associated with the property getter.

  • set

    The annotation gets associated with the property setter.

  • receiver

    The annotation gets associated with the receiver parameter of an extension function or property.

  • param

    The annotation gets associated with a constructor parameter.

  • setparam

    The annotation gets associated with a property setter parameter.

  • delegate

    The annotation gets associated with the field storing the delegate instance.

If you don’t specify a use-site target, the @Target meta-annotation is used to find the element to annotate. If there are several possibilities, the ranking is param > property > field.

The following code shows various annotation application examples (for simplicity, all annotations are without parameters and are presumed to have the correct @Target specified):
// An annotation applying to a file (the implicit
// internal class generated)
@file:Annot
package com.xyz.project
...
// An annotation applying to a class, a singleton
// object, or an interface
@Annot class TheName ...
@Annot object TheName ...
@Annot interface TheName ...
// An annotation applying to a function
@Annot fun theName() { ... }
// An annotation applying to a property
@property:Annot val theName = ...
@Annot var theName = ...
class SomeClass(@property:Annot var param:Type, ...) ...
// An annotation applying to a function parameter
f(@Annot p:Int, ...) { ... }
// An annotation applying to a constructor
class TheName @annot constructor(...) ...
// An annotation applying to a constructor parameter
class SomeClass(@param:Annot val param:Type, ...) ...
// An annotation applying to a lambda function
val f = @annot { par:Int -> ... }
// An annotation applying to the data field
// behind a property
@field:Annot val theName = ...
class SomeClass(@field:Annot val param:Type, ...) ...
// An annotation applying to a property setter
@set:Annot var theName = ...
var theName = 37 @Annot set(...) { ... }
class SomeClass(@set:Annot var param:Type, ...) ...
// An annotation applying to a property getter
@get:Annot var theName = ...
var theName = 37 @Annot get() = ...
class SomeClass(@get:Annot var param:Type, ...) ...
// An annotation applying to a property setter
// parameter
var theName:Int = 37
    set(@setparam:Annot p:String) { })
// An annotation applying to a receiver
@receiver:Annot fun String.xyz() { }
// An annotation applying to a delegate
class Derived(@delegate:Annot b: Base) : Base by b
To use annotations as annotation parameters, you don’t add a @ prefix:
@Annot(AnotherAnnot)

Annotations with Array Parameter

Using arrays as an annotation constructor parameter is easy: Just use the vararg qualifier in the annotation declaration, and in the annotation instantiation use a comma-separated parameter list:
annotation class Annot(vararg val params:String)
...
@Annot("A", "B", "C", ...) val prop:Int = ...
If you need to use an annotation with a single array parameter from a Java library you included in your project, the parameter gets automatically converted to a vararg parameter, so you basically do the same:
@field:JavaAnnot("A", "B", "C", ...) val prop:Int = ...
If annotations have several named parameters with one or several of them being an array, you use the special array literal notation:
@Annot(param1 = 37, arrParam = [37, 42, 6], ...)

Reading Annotations

For reading annotations with retention type SOURCE you need a special annotation processor. Remember that for SOURCE type annotation the Kotlin compiler removes the annotation during the compilation step, so in this case we must have some software looking at the sources before the compiler does its work. Most source type annotation processing happens inside bigger server framework projects; here the annotations get used to produce some synthetic Kotlin or Java code that glues together classes to model complex database structures. There is a special plug-in to be used for such purposes, KAPT, which allows for the inclusion of such source type annotation preprocessors.

You can find more information about KAPT usage in the online Kotlin documentation. For the rest of this section we talk about RUNTIME retention type annotation processing.

For reading annotations that have been compiled by the Kotlin compiler and ended up in the bytecode that gets executed by the runtime engine, the reflection API gets used. We discuss the reflection API later in this book; here we mention only annotation processing aspects.

Note

To use reflection, the kotlin-reflect.jar must be in the class path. This means you have to add implementation "org.jetbrains.kotlin: kotlin-reflect:$kotlin_version" inside the dependencies section of your module’s build.gradle file.

To get the annotations for the most basic elements, see Table 14-1, which describes how to get an annotation or a list of annotations.
Table 14-1.

Annotations by Element

Element

Reading Annotations

Classes, singleton objects, and interfaces

Use

TheName::class.annotations

to get a list of kotlin.Annotation objects you can further investigate. You can, for example, use the property .annotationClass to get the class of each annotation. If you have a property and first need to get the corresponding class, use

property::class.annotations

To read a certain annotation, use

val annot = TheName::class.findAnnotation<AnnotationType>()

where for AnnotationType you substitute the annotation’s class name. From here you can, for example, read an annotation’s parameter via annot?.paramName.

Properties

Use

val prop = ClassName::propertyNameval annots = prop.annotationsval annot = prop.findAnnotation<AnnotationType>()

to fetch a property by name and from there get a list of annotations or search for a certain annotation.

Fields

To access a field’s annotations use

val prop = ClassName::propertyNameval field = prop.javaFieldval annotations = field?.annotations

Functions

To access a nonoverloaded function by name write TheClass::functionName. In case you have several functions using the same name but with different parameters you can write

val funName = "functionName"    // <- choose your ownval pars = listOf(Int::class)    // <- choose your ownval function =     TheClass::class.    declaredFunctions.filter {        it.name == funName }    ?.find { f ->      val types = f.valueParameters.map{          it.type.jvmErasure}      types == pars

}

Once you have the function, you can use .annotations for a list of annotations, or .findAnnotation<AnnotationType>() to search for a certain annotation.

Built-in Annotations

Kotlin provides a couple of annotations from the start. Table 14-2 shows some general-purpose annotations.
Table 14-2.

Built-in Annotations: General

Annotation Name

Package

Targets

Description

Deprecated

kotlin

class, annotation class, function, property, constructor, property setter, property getter, type alias

Takes three parameters: message:String, replaceWith:ReplaceWith = ReplaceWith("")  and level:DeprecationLevel = DeprecationLevel.WARNING Mark the element as deprecated. DeprecationLevel is an enumeration with fields: WARNING, ERROR,  HIDDEN

ReplaceWith

kotlin

Takes two parameters: expression:String and vararg imports:String. Use this to specify a replacement code snippet inside @Deprecated.

Suppress

kotlin

class, annotation class, function, property, field, local variable, value parameter, constructor, property setter, property getter, type, type alias, expression, file

Takes one vararg parameter: names:String. Retention type is SOURCE. Use this to suppress compiler warnings. The names parameter is a comma-separated list of warning message identifiers. Unfortunately finding an exhaustive list of compiler warning identifiers is not that easy, but Android Studio helps: Once a compiler warning appears, the corresponding construct gets highlighted and pressing Alt+Enter with the cursor over it allows us to generate a corresponding suppress annotation. See Figure 14-1 (use arrow keys to navigate in the menu).

../images/476388_1_En_14_Chapter/476388_1_En_14_Fig1_HTML.jpg
Figure 14-1.

Suppress annotation in Android Studio

Custom Annotations

To define your own simple annotations, you write
@Target(...)
@Retention(...)
@Repeatable
@MustBeDocumented
annotation class AnnotationName

For the annotations for the annotation (i.e., the meta-annotations), note that they are all optional and the order is free. For their meanings, see the section “Annotation Characteristics” earlier in this chapter.

If you need annotations with parameters, you add a primary constructor to the declaration:
[possibly meta-annotations]
annotation class AnnotationName(val p1:Type1, val p2:Type2, ...)

where the following parameter types are allowed: types that correspond to primitive types (i.e., Byte, Short, Int, Long, Char, Float, Double), strings, classes, enums, other annotations, and arrays of those. You can add vararg for a variable number of arguments. Note that for annotations used as parameters for other annotations, the @ for the parameter annotations gets omitted.

As an example, we start a calculation engine in the form of a class Calculator. We introduce an annotation to avoid division by 0.0. The annotation reads:
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotZero()
For the class and two operators divide and multiply we write:
class Calculator {
  enum class Operator(val oper:String) {
      MULTIPLY("multiply"),
      DIVIDE("divide")
  }
  fun operator(oper:Operator,
               vararg params:Double): Double {
      val f = Calculator::class.declaredFunctions.
            find { it.name == oper.oper }
      f?.valueParameters?.forEachIndexed { ind, p ->
          p.findAnnotation<NotZero>()?.run {
              if (params[ind] == 0.0)
                  throw RuntimeException(
                  "Parameter ${ind} not unequal 0.0")
          }
      }
      val ps = arrayOf(this@Calculator,
            *(params).toList().toTypedArray<Any>())
      return (f?.call(*ps) as Double?) ?: 0.0
  }
  fun multiply(p1:Double, p2:Double) : Double {
      return p1 * p2
  }
  fun divide(p1:Double, @NotZero p2:Double) : Double {
      return p1 / p2
  }
}
The operator() function acts as follows:
  • It finds the function corresponding to the first parameter. The Calculator::class.declaredFunctions lists all the directly declared functions of the Calculator class. This means it does not also look into superclasses. The find selects divide() or multiply().

  • From the function, we loop though the parameters via .valueParameters. For each parameter, we see whether it has annotation NotZero associated with it. If it does, we check the actual parameter, and if it is 0.0, we throw an exception.

  • If no exception was thrown, we invoke the function. The arrayOf() expression concatenates the receiver object and the function parameters into a single Array<Any>.

The @NotZero annotation makes sure the parameter gets checked when Calculator.operator() is called. To use the calculator, you write something like this:
Calculator().
    operator(Calculator.Operator.DIVIDE,
            1.0, 1.0)

To see whether the annotation works, try another invocation with 0.0 as the second parameter.

Exercise 1

To the Calculator example, add a new annotation @NotNegative and a new operation sqrt() for the square root. Make sure a negative parameter for this operator is not allowed. Note: The actual square root gets calculated via java.lang.Math.sqrt().

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.31.240