Table of contents
The most common use case for using annotations is enforcing certain constraints, for example:
In this example, the date
field is a string, but what if we want to restrict the format of the data stored in the data class? Annotations come to the rescue.
To create this, we must define an annotation class:
annotation class AllowedRegex(val regex: String)
The annotation class must also be appropriately annotated.
Some of the common annotations include:
@Target
: Specifies the elements our annotation can be applied to. The target can be one of the following:
@Target(AnnotationTarget.FUNCTION)
@Target(AnnotationTarget.FIELD)
@Target(AnnotationTarget.CLASS)
@Target(AnnotationTarget.CONSTRUCTOR)
And so on.
@MustBeDocumented
: This annotation ensures that the annotation class is documented in the generated documentation.
@Retention
: This annotation specifies where and when our annotation class is accessible. The default value is@Retention(AnnotationRetention.RUNTIME)
.AnnotationRetention.BINARY
is useful when we only want our annotations reflected in the bytecode during compile time. It's typically used in combination with the@Keep
annotation for ProGuard rules, indicating not to obfuscate the data class. Since ProGuard operates on the final Java file,@Keep
annotation usesAnnotationRetention.BINARY
.
@Repeatable
: This annotation allows you to use your custom annotation multiple times on the same field.
Now, let’s continue with our example:
data class User(
val username: String,
@AllowedRegex("\\d{4}-\\d{2}-\\d{2}") val birthDate: String
)
Here, we restrict the birthDate
field using the applied annotation's regex string.
kotlinCopy code
data class User(
val username: String,
@AllowedRegex("\\d{4}-\\d{2}-\\d{2}") val birthDate: String
) {
init {
val fields = this::class.java.declaredFields
fields.forEach { field ->
field.annotations.forEach {
if (field.isAnnotationPresent(AllowedRegex::class.java)) {
val regex = field.getAnnotation(AllowedRegex::class.java)?.regex
if (regex?.toRegex()?.matches(birthDate) == false) {
throw IllegalArgumentException("Birthdate is not valid")
}
}
}
}
}
}
Example 2:
Custom annotations for Retrofit APIs. Sometimes, out of all the APIs implemented in our application, some require authentication, while others don’t. For this use case, we can create a custom annotation to intercept requests that need an auth header and automatically attach it to the request.
interface MyApi {
@GET("/users")
suspend fun getUsers() // Doesn't require authentication.
@Get("/posts/1")
@Authenticated
suspend fun getPostsForUser() // Requires authentication.
}
@Target(AnnotationTarget.FUNCTION)
annotation class Authenticated
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val invocation = chain.request().tag(Invocation::class.java)
?: return chain.proceed(chain.request())
val shouldAttachAuthHeader = invocation
.method()
.annotations
.any { it.annotationClass == Authenticated::class.java }
return if (shouldAttachAuthHeader) {
chain.proceed(
chain.request()
.newBuilder()
.addHeader("Authorization", "token")
.build()
)
} else chain.proceed(chain.request())
}
}
private val api by lazy {
Retrofit.Builder()
.baseUrl("url")
.client(
OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
)
.build()
.create(MyApi::class)
}
And it’s done! For a large project with APIs that can be differentiated into authenticated and non-authenticated, this can be a clean way to attach headers to your routes.
Thank you for reading this far, and have a great time!