1. Introduction

The Kotlin standard library provides an opt-in mechanism for requiring and giving explicit consent when it comes to using certain elements of APIs. This allows library developers to provide users with information about specific conditions that require user consent, such as when an API is in an experimental state and is likely to undergo changes in the future.

In this tutorial, we’ll learn more about Kotlin’s opt-in mechanism.

2. Opt-in

The opt-in requirement is an annotation-based declaration that allows us to mark certain elements of an API that should be either unusable without consent or used carefully by users of the API. Marking elements of an API such as a class or function as @RequiresOptIn means using these elements will yield a warning or an error in the code prompting the user to explicitly opt-in to using them. Doing so guarantees that a conscious decision to use the API has been made by the client code.

Opt-in APIs are pertinent in building libraries, but they also apply if we wish to control the usage of APIs in a multi-module project.

Prior to Kotlin 1.7, the @RequiresOptIn annotation itself was still experimental and produced warnings. So, in order to get rid of these warnings and use this feature in old versions of Kotlin, we need to configure the Kotlin compiler by adding the following code to our module’s build configuration:

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
        freeCompilerArgs += [
            '-Xopt-in=kotlin.RequiresOptIn',
        ]
    }
}

3. Creating Opt-in APIs

Now, let’s see how we can create APIs that compel users to opt-in to using them. We do this in two steps:

  • Creating an opt-in requirement annotation
  • Marking APIs as “opt-in required” with this annotation

3.1. Creating an Opt-in Requirement Annotation

If we want to require explicit consent to use an API, all we need to do is to create an annotation class to use as a @RequiresOptIn annotation:

@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MyClass

The @RequiresOptIn annotation must meet a few requirements:

  • BINARY or RUNTIME retention
  • The targets cannot include EXPRESSION, FILE, TYPE, or TYPE_PARAMETER
  • No parameters

In addition to these, a @RequiresOptIn can have one of two severity levels:

  • RequiresOptIn.Level.ERROR will cause a compilation error on the call site. Anytime we attempt to use an API that has this opt-in level, we must explicitly @OptIn to use it. Otherwise, the code won’t compile.

  • RequiresOptIn.Level.WARNING will show a yellow warning on the call site and during compilation. This is a “soft” warning and so it does not jeopardize compilation in any way as the compiler simply ignores it.

To set the desired level, specify the level parameter of the @RequiresOptIn annotation.

Additionally, we can also set a message that describes special conditions for using the API for users. The compiler will show this when the API is used without @OptIn.

For example, let’s create an annotation to an API that is still experimental in a library but is expected to become stable in later versions:

@RequiresOptIn(
    level = RequiresOptIn.Level.WARNING,
    message = "This is an experimental API. It may be changed or removed in the future."
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalClassApi

3.2. Marking APIs as Opt-in Required

Now, having created a marker annotation, it’s possible to mark certain pieces of our API with our annotation.

Let’s annotate a function with the ExperimentalClassApi annotation we created above:

@ExperimentalClassApi
fun someExperimentalApi(): Unit {
}

Wherever we call this method, we’ll receive a warning:

Warning message from using an opt-in annotation class with a warning severity level.

Furthermore, we can choose to place this annotation at the class level instead of just the method level. Doing this will still yield the appropriate warning message when the class is used.

4. Using Opt-in APIs

In this section, we’ll discuss various ways of handling the errors and warnings produced by calling opt-in APIs.

4.1. Propagating the Opt-in Requirement

Sometimes, we may not want to handle the error or warning generated by using an opt-in required function or class directly. In these cases, we’ll need to propagate the original opt-in requirement for downstream users to handle themselves.

For instance, considering our example from above, we can make the warning message created by the call to someExperimentalApi() disappear by creating a new method that calls someExperimentalApi() and marking this new method with the same @ExperimentalClassApi annotation:

@ExperimentalClassApi
fun anotherExperimentalApi() {
    someExperimentalApi()
}

By propagating the opt-in requirement, we allow the “warning-free” use of someExperimentalApi() method from within the implementation of anotherExperimentalApi() method. However, any user of the new method will have to equally deal with the opt-in requirement.

This method of propagating the opt-in requirement is useful when writing our code to make sure that we don’t accidentally create unmarked APIs that rely on experimental APIs in their implementation. If our class or methods rely on some experimental feature, they, too, should be experimental.

On the other hand, we should avoid propagating in client code as that will yield plenty of boilerplate code and tedium. Instead, we should @OptIn there. We’ll talk about that next.

4.2. Opting in an Entire Module

It is possible to enable all usages of an opt-in API throughout a module. All we need to do is use the -Xopt-in compiler option tallied with the fully qualified name of the annotation as the value:

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
        freeCompilerArgs += [
            '-Xopt-in=com.example.lib.core.ExperimentalClassApi',
        ]
    }
}

We must apply this method with care as enabling a certain opt-in API across a module can lead to creating many usages of the said API without additional warnings.

4.3. Opting in Locally

The last method we’ll discuss involves opting in locally. That is to say, we annotate the calling site where an opt-in API is invoked with the @OptIn annotation:

@ExperimentalClassApi
fun someExperimentalApi(): Unit {
}

@OptIn(ExperimentalClassApi::class)
var i = someExperimentalApi()

We can @OptIn at the statement level as seen above. However, we could decide to opt-in on other scopes such as an entire method:

@OptIn(ExperimentalClassApi::class)
fun OptInOnEntireFunction(){
    someExperimentalApi()
}

Or we can even opt-in on an entire file:

@file:OptIn(ExperimentalClassApi::class)
package com.baeldung.opt.requirement
class OptInRequirementsDemo {
    @ExperimentalClassApi
    fun someExperimentalApi(): Unit {
    }
    fun anotherExperimentalApi() {
        someExperimentalApi()
    }
    var i = someExperimentalApi()
}

5. Conclusion

In this article, we’ve discussed extensively what Kotlin’s Opt-in feature is about. We also showed how we can create our own opt-in requirement in an API, as well as how we can use an API that requires opting in. We went further to discuss various ways of handling warnings and errors imposed by the use of certain APIs.

As usual, all code samples used in this article are available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments