docs/api_guidelines/kotlin.md
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:
<pre> public final class TargetAnimation { ctor public TargetAnimation(float target, androidx.animation.AnimationBuilder animation); <b>method public float component1();</b> <b>method public androidx.animation.AnimationBuilder component2();</b> <b>method public androidx.animation.TargetAnimation copy(float target, androidx.animation.AnimationBuilder animation);</b> method public androidx.animation.AnimationBuilder getAnimation(); method public float getTarget(); } </pre>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 {#exhaustive-when}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 classKotlin'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 classAbstract 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; 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.
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.