Generally speaking, Kotlin code should follow the compatibility guidelines outlined at:
All projects in AndroidX compile using the same version of the Kotlin compiler -- typically the latest stable version -- and by default use a matching target language version. The target language version specifies which Kotlin features may be used in source code, which in turn specifies (1) which version of kotlin-stdlib
is used as a dependency and thus (2) which version of the Kotlin compiler is required when the library is used as a dependency.
Libraries may specify kotlinTarget
in their build.gradle
to override the default target language version. Using a higher language version will force clients to use a newer, typically less-stable Kotlin compiler but allows use of newer language features. Using a lower language version will allow clients to use an older Kotlin compiler when building their own projects.
androidx { kotlinTarget = KotlinVersion.KOTLIN_1_7 }
NOTE The client's Kotlin compiler version is bounded by their transitive dependencies. If your library uses target language version 1.7 but you depend on a library with target language version 1.9, the client will be forced to use 1.9 or higher.
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 @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.
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:
@Nullable
or @NonNull
in the library. Treat types with unknown nullability passed into or return from Android as @Nullable
in the library.@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.
@RecentlyNonNull
and @RecentlyNullable
APIsPlatform 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; }
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.
Always prefer non-null for Flow
objects, return a Flow that does not emit items as a default. One option is emptyFlow()
which will complete. Another option is flow { awaitCancellation() }
which will not emit and not complete. Choose the option that best suites the use-case.
fun myFlowFunction(): Flow<Data> { return if (canCreateFlow()) { createFlow() } else { emptyFlow() } }
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!" }
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
or compatibility with Java is required.
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.
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:
Consider using a non-exhaustive type declaration if:
The CommandResult
example above is a good example of a type that should use the exhaustive enum class
; CommandResult
s 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.
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.
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.
When the core type is in Java, one good use for extension functions is to create more Kotlin friendly versions of the API.
public class MyClass { public void addMyListener(Executor e, Consumer<Data> listener) { ... } public void removeMyListener(Consumer<Data> listener) { ... } }
fun MyClass.dataFlow(): Flow<Data>
When the core type is in Kotlin, extension functions may or may not be a good fit for the API. Ask if the extension is part of the core abstraction or a new layer of abstraction. One example of a new layer of abstraction is using a new dependency that does not otherwise interact with the core class. Another example is an abstraction that stands on its own such as a Comparator
that implements a non-canonical ordering.
In general when adding extension functions, consider splitting them across different files and naming the Java version of the files related to the use case as opposed to putting everything in one file and using a Util
suffix.
@file:JvmName("WindowSizeClassUtil") fun Set<WindowSizeClass>.widestClass() : WindowSizeClass { ... } fun WindowSizeClass.scoreWithinWidthDp(widthDp: Int) { ... }
@file:JvmName("WindowSizeClassSelector") fun Set<WindowSizeClass>.widestClass() : WindowSizeClass { ... } // In another file @file:JvmName("WindowSizeClassScoreCalculator") fun WindowSizeClass.scoreWithinWidthDp(widthDp: Int) { ... }
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:
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:
all
conversionall
conversion@JvmDefaultWithCompatibility
interfaceUnstable 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.