Annotations

Annotation processors

Annotation processors should opt-in to incremental annotation processing to avoid triggering a full recompilation on every client source code change. See Gradle's Incremental annotation processing documentation for information on how to opt-in.

@RequiresOptIn APIs

Jetpack libraries may choose to annotate API surfaces as unstable using either Kotlin‘s @RequiresOptIn meta-annotation for APIs written in Kotlin or Jetpack’s @RequiresOptIn meta-annotation for APIs written in Java.

@RequiresOptIn at-a-glance:

  • Use for unstable API surfaces
  • Can be called by anyone
  • Documented in public documentation
  • Does not maintain compatibility

For either annotation, API surfaces marked as opt-in are considered alpha and will be excluded from API compatibility guarantees. Due to the lack of compatibility guarantees, stable libraries must never call experimental APIs exposed by other libraries outside of their same-version group and may not use the @OptIn annotation except in the following cases:

  • A library within a same-version group may call an experimental API exposed by another library within its same-version group. In this case, API compatibility guarantees are covered under the same-version group policies and the library may use the @OptIn annotation to prevent propagation of the experimental property. Library owners must exercise care to ensure that post-alpha APIs backed by experimental APIs actually meet the release criteria for post-alpha APIs.
  • An alpha library may use experimental APIs from outside its same-version group. These usages must be removed when the library moves to beta.

NOTE JetBrains's own usage of @RequiresOptIn in Kotlin language libraries varies and may indicate binary instability, functional instability, or simply that an API is really difficult to use. Jetpack libraries should treat instances of @RequiresOptIn in JetBrains libraries as indicating binary instability and avoid using them outside of alpha; however, teams are welcome to obtain written assurance from JetBrains regarding binary stability of specific APIs. @RequiresOptIn APIs that are guaranteed to remain binary compatible may be used in beta, but usages must be removed when the library moves to rc.

When to mark an API surface as experimental

Do not use @RequiresOptIn for a stable API surface that is difficult to use. It is not a substitute for a properly-designed API surface.

Do not use @RequiresOptIn for an API surface that is unreliable or unstable because it is missing tests. It is not a substitute for a properly-tested API surface, and all APIs -- including those in alpha -- are expected to be functionally stable.

Do not use @RequiresOptIn for an internal-facing API surface. Use either the appropriate language visibility (ex. private or internal) or @RestrictTo.

Do not use @RequiresOptIn for an API that you expect library developers to call. Experimental APIs do not maintain binary compatibility guarantees, and you will put external clients in a difficult situation.

Do use @RequiresOptIn for API surfaces that must be publicly available and documented but need the flexibility to stay in alpha (and break compatibility) during the rest of the library's beta, rc, or stable cycles.

How to mark an API surface as experimental

All libraries using @RequiresOptIn annotations must depend on the androidx.annotation:annotation-experimental artifact regardless of whether they are using the androidx or Kotlin annotation. This artifact provides Lint enforcement of experimental usage restrictions for Kotlin callers as well as Java (which the Kotlin annotation doesn‘t handle on its own, since it’s a Kotlin compiler feature). Libraries may include the dependency as api-type to make @OptIn available to Java clients; however, this will also unnecessarily expose the @RequiresOptIn annotation.

dependencies {
    implementation(project(":annotation:annotation-experimental"))
}

See Kotlin‘s opt-in requirements documentation for general usage information. If you are writing experimental Java APIs, you will use the Jetpack @RequiresOptIn annotation rather than the Kotlin compiler’s annotation.

How to transition an API out of experimental

When an API surface is ready to transition out of experimental, the annotation may only be removed during an alpha pre-release stage. Removing the experimental marker from an API is equivalent to adding the API to the current API surface.

When transitioning an entire feature surface out of experimental, you should remove the definition for the associated experimental marker annotation.

When making any change to the experimental API surface, you must run ./gradlew updateApi prior to uploading your change.

NOTE Experimental marker annotation are themselves experimental, meaning that it's considered binary compatible to refactor or remove an experimental marker annotation.

@RestrictTo APIs

Jetpack's library tooling supports hiding Java-visible (ex. public and protected) APIs from developers using a combination of the @RestrictTo source annotation, and the @hide docs annotation (@suppress in Kotlin). These annotations must be paired together when used, and are validated as part of presubmit checks for Java code.

@RestrictTo at-a-glance:

  • Use for internal-facing API surfaces
  • Can be called within the specified Scope
  • Does not appear in public documentation
  • Does not maintain compatibility in most scopes

While restricted APIs do not appear in documentation and Android Studio will warn against calling them, hiding an API does not provide strong guarantees about usage:

  • There are no runtime restrictions on calling hidden APIs
  • Android Studio will not warn if hidden APIs are called using reflection
  • Hidden APIs will still show in Android Studio's auto-complete

When to use @hide

In other cases, avoid using @hide / @suppress. These annotations indicates that developers should not call an API that is technically public from a Java visibility perspective. Hiding APIs is often a sign of a poorly-abstracted API surface, and priority should be given to creating public, maintainable APIs and using Java visibility modifiers.

Do not use @hide/@suppress to bypass API tracking and review for production APIs; instead, rely on API+1 and API Council review to ensure APIs are reviewed on a timely basis.

Do not use @hide/@suppress for implementation detail APIs that are used between libraries and could reasonably be made public.

Do use @hide/@suppress paired with @RestrictTo(LIBRARY) for implementation detail APIs used within a single library (but prefer Java language private or default visibility).

RestrictTo.Scope and inter- versus intra-library API surfaces

To maintain binary compatibility between different versions of libraries, restricted API surfaces that are used between libraries within Jetpack (inter-library APIs) must follow the same Semantic Versioning rules as public APIs. Inter-library APIs should be annotated with the @RestrictTo(LIBRARY_GROUP) source annotation and @hide docs annotation.

Restricted API surfaces used within a single library (intra-library APIs), on the other hand, may be added or removed without any compatibility considerations. It is safe to assume that developers never call these APIs, even though it is technically feasible. Intra-library APIs should be annotated with the @RestrictTo(LIBRARY) source annotation and @hide docs annotation.

In all cases, correctness and compatibility tracking are handled by AndroidX's build system and lint checks.

The following table shows the visibility of a hypothetical API within Maven coordinate androidx.concurrent:concurrent when annotated with a variety of scopes:

@IntDef @StringDef and @LongDef and visibility

All @IntDef, @StringDef, and @LongDef will be stripped from resulting artifacts to avoid issues where compiler inlining constants removes information as to which @IntDef defined the value of 1. The annotations are extracted and packaged separately to be read by Android Studio and lint which enforces the types in application code.

  • Libraries must @hide all @IntDef, @StringDef, and @LongDef declarations.
  • Libraries must expose constants used to define the @IntDef etc at the same Java visibility as the hidden @IntDef
  • Libraries must use @RestrictTo to create a warning when the type is used incorrectly.

Here is a complete example of an @IntDef

// constants match Java visibility of ExifStreamType
// code outside this module interacting with ExifStreamType uses these constants
public static final int STREAM_TYPE_FULL_IMAGE_DATA = 1;
public static final int STREAM_TYPE_EXIF_DATA_ONLY = 2;

/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY) // Don't export ExifStreamType outside module
@Retention(RetentionPolicy.SOURCE)
@IntDef({
  STREAM_TYPE_FULL_IMAGE_DATA,
  STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}

Java visibilty should be set as appropriate for the code in question (private, package or public) and is unrelated to hiding.

For more, read the section in Android API Council Guidelines

*current.txt File Explanation

In this example, 1.3.0-beta02.txt is just used for an example. This will match the current library version.

Constructors

View constructors

The four-arg View constructor -- View(Context, AttributeSet, int, int) -- was added in SDK 21 and allows a developer to pass in an explicit default style resource rather than relying on a theme attribute to resolve the default style resource. Because this API was added in SDK 21, care must be taken to ensure that it is not called through any < SDK 21 code path.

Views may implement a four-arg constructor in one of the following ways:

  1. Do not implement.
  2. Implement and annotate with @RequiresApi(21). This means the three-arg constructor must not call into the four-arg constructor.

Asynchronous work

With return values

Kotlin

Traditionally, asynchronous work on Android that results in an output value would use a callback; however, better alternatives exist for libraries.

Kotlin libraries should consider coroutines and suspend functions for APIs according to the following rules, but please refer to the guidance on allowable dependencies before adding a new dependency on coroutines.

Kotlin suspend fun vs blockingBehavior
blocking function with @WorkerThreadAPI is blocking
suspendAPI is async (e.g. Future)

In general, do not introduce a suspend function entirely to switch threads for blocking calls. To do so correctly requires that we allow the developer to configure the Dispatcher. As there is already a coroutines-based API for changing dispatchers (withContext) that the caller may use to switch threads, it is unecessary API overhead to provide a duplicate mechanism. In addition, it unecessary limits callers to coroutine contexts.

// DO expose blocking calls as blocking calls
@WorkerThread
fun blockingCall()

// DON'T wrap in suspend functions (only to switch threads)
suspend fun blockingCallWrappedInSuspend(
  dispatcher: CoroutineDispatcher = Dispatchers.Default
) = withContext(dispatcher) { /* ... */ }

// DO expose async calls as suspend funs
suspend fun asyncCall(): ReturnValue

// DON'T expose async calls as a callback-based API (for the main API)
fun asyncCall(executor: Executor, callback: (ReturnValue) -> Unit)

Java

Java libraries should prefer ListenableFuture and the CallbackToFutureAdapter implementation provided by the androidx.concurrent:concurrent-futures library. Functions and methods that return ListenableFuture should be suffixed by, Async to reserve the shorter, unmodified name for a suspend method or extension function in Kotlin that returns the value normally in accordance with structured concurrency.

Libraries must not use java.util.concurrent.CompletableFuture, as it has a large API surface that permits arbitrary mutation of the future's value and has error-prone defaults.

See the Dependencies section for more information on using Kotlin coroutines and Guava in your library.

Cancellation

Libraries that expose APIs for performing asynchronous work should support cancellation. There are very few cases where it is not feasible to support cancellation.

Libraries that use ListenableFuture must be careful to follow the exact specification of Future.cancel(boolean mayInterruptIfRunning) behavior.

@Override
public boolean cancel(boolean mayInterruptIfRunning) {
    // Does not support cancellation.
    return false;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
    // Aggressively does not support cancellation.
    throw new UnsupportedOperationException();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
    // Pseudocode that ignores threading but follows the spec.
    if (mCompleted
            || mCancelled
            || mRunning && !mayInterruptIfRunning) {
        return false;
    }
    mCancelled = true;
    return true;
}

Avoid synchronized methods

Whenever multiple threads are interacting with shared (mutable) references those reads and writes must be synchronized in some way. However synchronized blocks make your code thread-safe at the expense of concurrent execution. Any time execution enters a synchronized block or method any other thread trying to enter a synchronized block on the same object has to wait; even if in practice the operations are unrelated (e.g. they interact with different fields). This can dramatically reduce the benefit of trying to write multi-threaded code in the first place.

Locking with synchronized is a heavyweight form of ensuring ordering between threads, and there are a number of common APIs and patterns that you can use that are more lightweight, depending on your use case:

  • Compute a value once and make it available to all threads
  • Update Set and Map data structures across threads
  • Allow a group of threads to process a stream of data concurrently
  • Provide instances of a non-thread-safe type to multiple threads
  • Update a value from multiple threads atomically
  • Maintain granular control of your concurrency invariants

Kotlin-specific guidelines

Generally speaking, Kotlin code should follow the compatibility guidelines outlined at:

Nullability

Annotations on new Java APIs

All new Java APIs should be annotated either @Nullable or @NonNull for all reference parameters and reference return types.

    @Nullable
    public Object someNewApi(@NonNull Thing arg1, @Nullable List<WhatsIt> arg2) {
        if(/** something **/) {
            return someObject;
        } else {
            return null;
    }

Adding annotations to existing Java APIs

Adding @Nullable or @NonNull annotations to existing APIs to document their existing nullability is allowed. This is a source-breaking change for Kotlin consumers, and you should ensure that it's noted in the release notes and try to minimize the frequency of these updates in releases.

Changing the nullability of an API is a behavior-breaking change and should be avoided.

Extending APIs that are missing annotations

Platform types are exposed by Java types that do not have a @Nullable or @NonNull annotation. In Kotlin they are indicated with the ! suffix.

When interacting with an Android platform API that exposes APIs with unknown nullability follow these rules:

  1. If wrapping the type in a new API, define and handle @Nullable or @NonNull in the library. Treat types with unknown nullability passed into or return from Android as @Nullable in the library.
  2. If extending an existing API (e.g. @Override), pass through the existing types with unknown nullability and annotate each with @SuppressLint("UnknownNullness")

In Kotlin, a type with unknown nullability is exposed as a “platform type” (indicated with a ! suffix) which has unknown nullability in the type checker, and may bypass type checking leading to runtime errors. When possible, do not directly expose types with unknown nullability in new public APIs.

Extending @RecentlyNonNull and @RecentlyNullable APIs

Platform APIs are annotated in the platform SDK artifacts with fake annotations @RecentlyNonNull and @RecentlyNullable to avoid breaking builds when we annotated platform APIs with nullability. These annotations cause warnings instead of build failures. The RecentlyNonNull and RecentlyNullable annotations are added by Metalava and do not appear in platform code.

When extending an API that is annotated @RecentlyNonNull, you should annotate the override with @NonNull, and the same for @RecentlyNullable and @Nullable.

For example SpannableStringBuilder.append is annotated RecentlyNonNull and an override should look like:

    @NonNull
    @Override
    public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text) {
        super.append(text);
        return this;
    }

Data classes

Kotlin data classes provide a convenient way to define simple container objects, where Kotlin will generate equals() and hashCode() for you. However, they are not designed to preserve API/binary compatibility when members are added. This is due to other methods which are generated for you - destructuring declarations, and copying.

Example data class as tracked by metalava:

Because members are exposed as numbered components for destructuring, you can only safely add members at the end of the member list. As copy is generated with every member name in order as well, you'll also have to manually re-implement any old copy variants as items are added. If these constraints are acceptable, data classes may still be useful to you.

As a result, Kotlin data classes are strongly discouraged in library APIs. Instead, follow best-practices for Java data classes including implementing equals, hashCode, and toString.

See Jake Wharton's article on Public API challenges in Kotlin for more details.

Exhaustive when and sealed class/enum class

A key feature of Kotlin's sealed class and enum class declarations is that they permit the use of exhaustive when expressions. For example:

enum class CommandResult { Permitted, DeniedByUser }

val message = when (commandResult) {
    Permitted -> "the operation was permitted"
    DeniedByUser -> "the user said no"
}

println(message)

This highlights challenges for library API design and compatibility. Consider the following addition to the CommandResult possibilities:

enum class CommandResult {
    Permitted,
    DeniedByUser,
    DeniedByAdmin // New in androidx.mylibrary:1.1.0!
}

This change is both source and binary breaking.

It is source breaking because the author of the when block above will see a compiler error about not handling the new result value.

It is binary breaking because if the when block above was compiled as part of a library com.example.library:1.0.0 that transitively depends on androidx.mylibrary:1.0.0, and an app declares the dependencies:

implementation("com.example.library:1.0.0")
implementation("androidx.mylibrary:1.1.0") // Updated!

com.example.library:1.0.0 does not handle the new result value, leading to a runtime exception.

Note: The above example is one where Kotlin's enum class is the correct tool and the library should not add a new constant! Kotlin turns this semantic API design problem into a compiler or runtime error. This type of library API change could silently cause app logic errors or data corruption without the protection provided by exhaustive when. See When to use exhaustive types.

sealed class exhibits the same characteristic; adding a new subtype of an existing sealed class is a breaking change for the following code:

val message = when (command) {
    is Command.Migrate -> "migrating to ${command.destination}"
    is Command.Quack -> "quack!"
}

Non-exhaustive alternatives to enum class

Kotlin‘s @JvmInline value class with a private constructor can be used to create type-safe sets of non-exhaustive constants as of Kotlin 1.5. Compose’s BlendMode uses the following pattern:

@JvmInline
value class BlendMode private constructor(val value: Int) {
    companion object {
        /** Drop both the source and destination images, leaving nothing. */
        val Clear = BlendMode(0)
        /** Drop the destination image, only paint the source image. */
        val Src = BlendMode(1)
        // ...
    }
}

Note: This recommendation may be temporary. Kotlin may add new annotations or other language features to declare non-exhaustive enum classes in the future.

Alternatively, the existing @IntDef mechanism used in Java-language androidx libraries may also be used, but type checking of constants will only be performed by lint, and functions overloaded with parameters of different value class types are not supported. Prefer the @JvmInline value class solution for new code unless it would break local consistency with other API in the same module that already uses @IntDef.

Non-exhaustive alternatives to sealed class

Abstract classes with constructors marked as internal or private can represent the same subclassing restrictions of sealed classes as seen from outside of a library module's own codebase:

abstract class Command private constructor() {
    class Migrate(val destination: String) : Command()
    object Quack : Command()
}

Using an internal constructor will permit non-nested subclasses, but will not restrict subclasses to the same package within the module, as sealed classes do.

When to use exhaustive types

Use enum class or sealed class when the values or subtypes are intended to be exhaustive by design from the API's initial release. Use non-exhaustive alternatives when the set of constants or subtypes might expand in a minor version release.

Consider using an exhaustive (enum class or sealed class) type declaration if:

  • The developer is expected to accept values of the type
  • The developer is expected to act on any and all values received

Consider using a non-exhaustive type declaration if:

  • The developer is expected to provide values of the type to APIs exposed by the same module only
  • The developer is expected to ignore unknown values received

The CommandResult example above is a good example of a type that should use the exhaustive enum class; CommandResults are returned to the developer and the developer cannot implement correct app behavior by ignoring unrecognized result values. Adding a new result value would semantically break existing code regardless of the language facility used to express the type.

enum class CommandResult { Permitted, DeniedByUser, DeniedByAdmin }

Compose's BlendMode is a good example of a type that should not use the exhaustive enum class; blending modes are used as arguments to Compose graphics APIs and are not intended for interpretation by app code. Additionally, there is historical precedent from android.graphics for new blending modes to be added in the future.

Extension and top-level functions

If your Kotlin file contains any symbols outside of class-like types (extension/top-level functions, properties, etc), the file must be annotated with @JvmName. This ensures unanticipated use-cases from Java callers don't get stuck using BlahKt files.

Example:

package androidx.example

fun String.foo() = // ...
@file:JvmName("StringUtils")

package androidx.example

fun String.foo() = // ...

NOTE This guideline may be ignored for APIs that will only be referenced from Kotlin sources, such as Compose.

Extension functions on platform classes

While it may be tempting to backport new platform APIs using extension functions, the Kotlin compiler will always resolve collisions between extension functions and platform-defined methods by calling the platform-defined method -- even if the method doesn't exist on earlier SDKs.

fun AccessibilityNodeInfo.getTextSelectionEnd() {
    // ... delegate to platform on SDK 18+ ...
}

For the above example, any calls to getTextSelectionEnd() will resolve to the platform method -- the extension function will never be used -- and crash with MethodNotFoundException on older SDKs.

Even when an extension function on a platform class does not collide with an existing API yet, there is a possibility that a conflicting API with a matching signature will be added in the future. As such, Jetpack libraries should avoid adding extension functions on platform classes.

Function paremeters order

In Kotlin function parameters can have default values, which are used when you skip the corresponding argument.

If a default parameter precedes a parameter with no default value, the default value can only be used by calling the function with named arguments:

fun foo(
    someBoolean: Boolean = true,
    someInt: Int,
) { /*...*/ }

// usage:
foo(1) // does not compile as we try to set 1 as a value for "someBoolean" and
       // didn't specify "someInt".
foo(someInt = 1) // this compiles as we used named arguments syntax.

To not force our users to use named arguments we enforce the following parameters order for the public Kotlin functions:

  1. All parameters without default values.
  2. All parameters with default values.
  3. An optional last parameter without default value which can be used as a trailing lambda.

Default interface methods

The Kotlin compiler is capable of generating Kotlin-specific default interface methods that are compatible with Java 7 language level; however, Jetpack libraries ship as Java 8 language level and should use the native Java implementation of default methods.

To maximize compatibility, Jetpack libraries should pass -Xjvm-default=all to the Kotlin compiler:

tasks.withType(KotlinCompile).configureEach {
    kotlinOptions {
        freeCompilerArgs += ["-Xjvm-default=all"]
    }
}

Before adding this argument, library owners must ensure that existing interfaces with default methods in stable API surfaces are annotated with @JvmDefaultWithCompatibility to preserve binary compatibility:

  1. Any interface with stable default method implementations from before the all conversion
  2. Any interface with stable methods that have default argument values from before the all conversion
  3. Any interface that extends another @JvmDefaultWithCompatibility interface

Unstable API surfaces do not need to be annotated, e.g. if the methods or whole interface is @RequiresOptIn or was never released in a stable library version.

One way to handle this task is to search the API .txt file from the latest release for default or optional and add the annotation by hand, then look for public sub-interfaces and add the annotation there as well.

Proguard configuration

Proguard configurations allow libraries to specify how post-processing tools like optimizers and shrinkers should operate on library bytecode. Note that while Proguard is the name of a specific tool, a Proguard configuration may be read by R8 or any number of other post-processing tools.

NOTE Jetpack libraries must not run Proguard on their release artifacts. Do not specify minifyEnabled, shrinkResources, or proguardFiles in your build configuration.

Bundling with a library

Android libraries (AARs) can bundle consumer-facing Proguard rules using the consumerProguardFiles (not proguardFiles) field in their build.gradle file's defaultConfig:

android {
    defaultConfig {
        consumerProguardFiles 'proguard-rules.pro'
    }
}

Libraries do not need to specify this field on buildTypes.all.

Java-only libraries (JARs) can bundle consumer-facing Proguard rules by placing the file under the META-INF resources directory. The file must be named using the library's unique Maven coordinate to avoid build-time merging issues:

<project>/src/main/resources/META-INF/proguard/androidx.core_core.pro