Alix
Alix

Reputation: 2837

Handling multiple experimental annotations throughout an app

I have an app that makes heavy use of experimental features for Jetpack Compose so I have to declare a bunch of annotations on the composables. Since these annotations require callers to also declare them I have ended up in a situation where I have an activity with the following code:

import androidx.appcompat.app.AppCompatActivity

import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.ui.ExperimentalComposeUiApi

import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.permissions.ExperimentalPermissionsApi
…

class MainActivity : AppCompatActivity() {

    @ExperimentalPermissionsApi
    @ExperimentalComposeUiApi
    @ExperimentalPagerApi
    @ExperimentalMaterialNavigationApi
    @ExperimentalMaterialApi
    override fun onCreate(savedInstanceState: Bundle?) {
        // … wiring up compose code (which propagates the experimental annotations)

An alternative to avoid this situation would be to use the @OptIn instead but since only one is allowed per declaration it doesn't work out for my case with multiple experimental features.

Any way… This works fine — In Kotlin 1.5.

With Kotlin 1.6 I am getting a compilation error:

Opt-in requirement marker annotation on override requires the same marker on base declaration

But the base declaration is in the standard API that I cannot change. How can I make this compile (and work as before)?

Upvotes: 21

Views: 12381

Answers (6)

okmanideep
okmanideep

Reputation: 1060

TLDR;

@ExperimentalAnimationApi
This annotation means - "This is an experimental API, all users have to explicitly opt in to use". Rarely the case for application developers.

@OptIn(ExperimetalAnimationApi::class)
This annotation means - "I am opting in to use an experimental api". It does not force the users of this method/class to add annotation in their code.

Opt-In Requirements | Kotlin


Problem

@ExperimentalAnimationApi
@Composable
fun MyCode()

means MyCode is experimental animation api, if you want to use MyCode, explicitly opt in to ExperimentalAnimationApi

@Composable
fun MyOtherCode() {
  MyCode() // ERROR!, doesn't compile and shows red underlines in IDE
}

👆 is what typically causes too many @ExperimentalAnimationApi annotations in our code. DO NOT DO THIS

Solution (fix compilation, IDE warnings)

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun MyCode() {
  // experimental animation api usage
}

👆 will NOT force the callers to add any annotations

@Composable
fun MyOtherCode() {
  MyCode() // safe, compiles and doesn't show any errors in IDE
}

As app developers, we almost always have to
use @OptIn(ExperimentalAnimationApi::class) and
NOT @ExperimentalAnimationApi,

unless our code itself is exposing experimental declarations in it's public surface as return types or function arguments etc.

Solution to only fix compilation

Alternatively we can add

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
  kotlinOptions {
    freeCompilerArgs = freeCompilerArgs + listOf(
      "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
    )
  }
}

but this doesn't remove the IDE underlines etc. So 👆 is not that useful.

Upvotes: 31

Raman
Raman

Reputation: 19585

With modern versions of the Kotlin Gradle multiplatform plugin, you should not manually configure this via freeCompilerArgs. You can use a strongly typed DSL via the sourceSets.languageSettings block. For example, with kotlin multiplatform, you might declare it as follows, using the Gradle Kotlin DSL:

  sourceSets {
    all {
      languageSettings.apply {
        optIn("kotlin.ExperimentalStdlibApi")
        optIn("kotlin.experimental.ExperimentalNativeApi")
        optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
      }
    }
  }

The languageSettings optIn DSL can also apply to individual source sets e.g.:

commonMain {
  languageSettings.apply {
    ...
  }
}

Upvotes: 0

KrisOch
KrisOch

Reputation: 11

In an multi-module project if you want to centraly define such things, but a given library isn't used in all of the sub-modules you can do it as below - dependent on the libraries you use e.g. when you use Compose set the Compose-Opt-Ins.

In the build.gradle.kts (root):

subprojects {

     afterEvaluate {
        if (configurations.any { it.incoming.dependencies.any { dep -> dep.group == "androidx.compose" } }) {
            tasks.withType<KotlinCompile>().all {
                kotlinOptions {
                    freeCompilerArgs = freeCompilerArgs + listOf(
                        "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
                        "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
                    )
                }
            }
        }
}

Upvotes: 0

Alix
Alix

Reputation: 2837

A non-deprecated variation of @Johanns answer in Kotlin DSL (with some other annotations that I'm using):

Deprecation warning:

w: '-Xuse-experimental' is deprecated and will be removed in a future release, please use '-opt-in' instead

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all {
        kotlinOptions {
            freeCompilerArgs = freeCompilerArgs + listOf(
                // Avoid having to stutter experimental annotations all over the codebase
                "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
                "-opt-in=androidx.compose.material.ExperimentalMaterialApi",
                "-opt-in=androidx.compose.runtime.ExperimentalComposeApi",
                "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
                "-opt-in=com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi",
                "-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
                "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
                "-opt-in=kotlin.ExperimentalUnsignedTypes",
                "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
                "-opt-in=kotlinx.coroutines.InternalCoroutinesApi"
            )
        }
    }

Upvotes: 15

Ohlsen
Ohlsen

Reputation: 195

An alternative to avoid this situation would be to use the @OptIn instead but since only one is allowed per declaration it doesn't work out for my case with multiple experimental features.

You can put multiple experimental features comma separated into @OptIn.

E.g. @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)

Upvotes: 9

Johann
Johann

Reputation: 29867

I got tired of my code being polluted by all those annotations. The easiest way to get rid of them and have your code compile is just add this to your top build.gradle file - It's not exhaustive. Just add more compiler arguments for each annotation you need:

allprojects {
    tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
        kotlinOptions {
            freeCompilerArgs += [
                    "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes",
                    "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
                    "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
                    "-Xuse-experimental=androidx.compose.animation.ExperimentalAnimationApi",
                    "-Xuse-experimental=androidx.compose.ExperimentalComposeApi",
                    "-Xuse-experimental=androidx.compose.material.ExperimentalMaterialApi",
                    "-Xuse-experimental=androidx.compose.runtime.ExperimentalComposeApi",
                    "-Xuse-experimental=androidx.compose.ui.ExperimentalComposeUiApi",
                    "-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
                    "-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
                    "-Xuse-experimental=com.google.accompanist.pager.ExperimentalPagerApi"
            ]
        }
    }
}

Upvotes: 14

Related Questions