Kotlin 2.3.0: Key Updates and New Features

Kotlin Updates

Discover the latest advancements in Kotlin 2.3.0, featuring language stability, Java 25 support, enhanced interoperability for Native and JS, improved Gradle tooling, and new standard library functionalities.

Kotlin 2.3.0 brings a wealth of enhancements across the platform, focusing on language stability, platform-specific improvements, and developer experience. This release introduces significant updates for various targets, including JVM, Native, Wasm, and JS, along with improvements to Gradle, the Compose compiler, and the standard library.

Key highlights of Kotlin 2.3.0 include:

  • Language: Enhanced feature stability, a new unused return value checker, explicit backing fields, and refinements to context-sensitive resolution.
  • Kotlin/JVM: Official support for Java 25.
  • Kotlin/Native: Improved Swift export interoperability, faster build times for release tasks, and Beta support for C and Objective-C library import.
  • Kotlin/Wasm: Fully qualified names and a new exception handling proposal enabled by default, plus compact storage for Latin-1 characters.
  • Kotlin/JS: Experimental suspend function export, LongArray representation via BigInt64Array, and unified companion object access.
  • Gradle: Compatibility with Gradle 9.0 and a new API for registering generated sources.
  • Compose Compiler: Stack traces for minified Android applications.
  • Standard Library: Stable time tracking (kotlin.time.Clock, kotlin.time.Instant) and improved UUID generation and parsing.

To update to Kotlin 2.3.0, simply adjust the Kotlin version in your build scripts; no IDE plugin update is required if you are using the latest versions of IntelliJ IDEA or Android Studio.

Language Enhancements

Kotlin 2.3.0 focuses on stabilizing existing features, introducing a new mechanism for detecting unused return values, and refining context-sensitive resolution.

Stable Features

Several language features previously introduced as Experimental or Beta have now transitioned to Stable in Kotlin 2.3.0:

  • Support for nested type aliases.
  • Data-flow-based exhaustiveness checks for when expressions.

Features Enabled by Default

In Kotlin 2.3.0, support for return statements in expression bodies with explicit return types is now enabled by default.

Unused Return Value Checker

Kotlin 2.3.0 introduces an experimental unused return value checker to help prevent silently dropped function results. This feature generates warnings when an expression returns a value (other than Unit or Nothing) that is not subsequently used, passed to another function, or evaluated in a condition. This helps developers identify and fix potential bugs where meaningful results are inadvertently ignored. The checker deliberately ignores values from increment/decrement operations (e.g., ++, --).

Consider the following example where a string is created but its return value is ignored:

fun formatGreeting(name: String): String {
    if (name.isBlank()) return "Hello, anonymous user!"
    if (!name.contains(' ')) {
        // The checker reports a warning that this result is ignored
        "Hello, " + name.replaceFirstChar(Char::titlecase) + "!"
    }
    val (first, last) = name.split(' ')
    return "Hello, $first! Or should I call you Dr. $last?"
}

To enable this experimental feature, add the following compiler option to your build file:

Gradle (Kotlin DSL):

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xreturn-value-checker=check")
    }
}

Maven:

<build>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <configuration>
                <args>
                    <arg>-Xreturn-value-checker=check</arg>
                </args>
            </configuration>
        </plugin>
    </plugins>
</build>

With -Xreturn-value-checker=check, the checker only reports ignored results for functions explicitly marked with the @MustUseReturnValues annotation or for most functions within the Kotlin standard library.

To mark your own functions or scopes:

  • Mark an entire file:
    // Marks all functions and classes in this file so the checker reports unused return values
    @file:MustUseReturnValues
    
    package my.project
    
    fun someFunction(): String
    
  • Mark a specific class:
    // Marks all functions in this class so the checker reports unused return values
    @MustUseReturnValues
    class Greeter {
        fun greet(name: String): String = "Hello, $name"
    }
    
    fun someFunction(): Int = ...
    

Alternatively, to mark your entire project, use the compiler option -Xreturn-value-checker=full:

Gradle (Kotlin DSL):

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xreturn-value-checker=full")
    }
}

Maven:

<build>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <configuration>
                <args>
                    <arg>-Xreturn-value-checker=full</arg>
                </args>
            </configuration>
        </plugin>
    </plugins>
</build>

With this setting, all compiled files in your project are treated as if annotated with @MustUseReturnValues, and the checker will report all return values for your project's functions.

You can suppress warnings for specific functions by annotating them with @IgnorableReturnValue, typically for functions where ignoring the return value is a common and expected pattern (e.g., MutableList.add).

@IgnorableReturnValue
fun <T> MutableList<T>.addAndIgnoreResult(element: T): Boolean {
    return add(element)
}

To suppress a warning at a specific call site without marking the function, assign the result to a special unnamed variable using an underscore (_):

// Non-ignorable function
fun computeValue(): Int = 42

fun main() {
    // Reports a warning: result is ignored
    computeValue()

    // Suppresses the warning only at this call site with a special unused variable
    val _ = computeValue()
}

Explicit Backing Fields

Kotlin 2.3.0 introduces experimental explicit backing fields, a new syntax for directly declaring the underlying field that stores a property's value. This contrasts with the previous implicit backing fields and simplifies the common pattern of having a property's internal type differ from its exposed API type (e.g., exposing an ArrayList as a read-only List).

This new syntax eliminates the need for a separate private property to manage the internal state. The implementation type of the field is defined directly within the property's scope, allowing the compiler to automatically perform smart casting to the backing field's type within that private scope.

Before:

private val _city = MutableStateFlow<String>("")
val city: StateFlow<String> get() = _city

fun updateCity(newCity: String) {
    _city.value = newCity
}

After (with explicit backing fields):

val city: StateFlow<String>
    field = MutableStateFlow("")

fun updateCity(newCity: String) {
    // Smart casting works automatically
    city.value = newCity
}

To enable this experimental feature, add the following compiler option to your build file:

Gradle (Kotlin DSL):

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xexplicit-backing-fields")
    }
}

Maven:

<build>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <configuration>
                <args>
                    <arg>-Xexplicit-backing-fields</arg>
                </args>
            </configuration>
        </plugin>
    </plugins>
</build>

Changes to Context-Sensitive Resolution

While still an experimental feature, context-sensitive resolution continues to evolve based on user feedback. In Kotlin 2.3.0, improvements include:

  • Expanded Contextual Scope: Sealed and enclosing supertypes of the current type are now included in the contextual search scope. Other supertype scopes are not considered.
  • Ambiguity Warnings: The compiler now issues warnings when type operators and equalities lead to ambiguous resolution due to context-sensitive resolution, such as when clashing class declarations are imported.

Kotlin/JVM: Java 25 Support

Kotlin 2.3.0 introduces full support for Java 25, enabling the compiler to generate classes compatible with Java 25 bytecode.

Kotlin/Native Enhancements

Kotlin 2.3.0 delivers significant improvements for Kotlin/Native, including enhanced Swift export capabilities, better C and Objective-C library import, and faster build times for release tasks.

Improved Interoperability through Swift Export

Kotlin 2.3.0 enhances Swift interoperability by introducing direct support for native enum classes and variadic function parameters.

Previously, Kotlin enums were exported as standard Swift classes. Now, Kotlin enums map directly to native Swift enums:

Kotlin:

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

val color = Color.RED

Swift:

public enum Color: Swift.CaseIterable, Swift.LosslessStringConvertible, Swift.RawRepresentable {
    case RED, GREEN, BLUE

    var rgb: Int { get }
}

Additionally, Kotlin's vararg functions are now directly mapped to Swift's variadic function parameters, simplifying the process of passing a variable number of arguments.

Kotlin:

fun log(vararg messages: String)

Swift:

public func log(messages: Swift.String...)

Note: Generic types in variadic function parameters are not yet supported.

Beta Support for C and Objective-C Library Import

Kotlin 2.3.0 introduces Beta support for importing C and Objective-C libraries into Kotlin/Native projects. While full compatibility with all versions of Kotlin, dependencies, and Xcode is still being refined, the compiler now provides improved diagnostics for binary compatibility issues.

This feature remains experimental, requiring the @OptIn(ExperimentalForeignApi::class) annotation for certain C and Objective-C interoperability aspects, such as specific APIs in the kotlinx.cinterop.* package and most declarations within native libraries (excluding platform libraries).

Default Explicit Names in Objective-C Block Types

Explicit parameter names in Kotlin's function types are now the default for Objective-C headers exported from Kotlin/Native. This enhancement improves autocompletion suggestions in Xcode and helps prevent Clang warnings.

Kotlin:

fun greetUser(block: (name: String) -> Unit) = block("John")

Objective-C (Xcode suggestions):

greetUserBlock:^(NSString *name) {
    // ...
};

Should you encounter issues, explicit parameter names can be disabled by adding kotlin.native.binary.objcExportBlockExplicitParameterNames=false to your gradle.properties file.

Faster Build Times for Release Tasks

Kotlin/Native 2.3.0 introduces significant performance improvements, leading to up to 40% faster build times for release tasks such as linkRelease* (e.g., linkReleaseFrameworkIosArm64). These optimizations are particularly beneficial for Kotlin Multiplatform projects targeting iOS.

Changes to Apple Target Support

Kotlin 2.3.0 updates the minimum supported versions for Apple targets:

  • iOS and tvOS: Minimum version raised from 12.0 to 14.0.
  • watchOS: Minimum version raised from 5.0 to 7.0.

This change streamlines maintenance and paves the way for future support of Mac Catalyst in Kotlin/Native. Developers needing to maintain older target versions can override these minimums in their build files, though successful compilation and runtime stability are not guaranteed:

kotlin {
    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>().configureEach {
        binaries.configureEach {
            freeCompilerArgs += "-Xoverride-konan-properties=minVersion.ios=12.0"
            freeCompilerArgs += "-Xoverride-konan-properties=minVersion.tvos=12.0"
        }
    }
}

Furthermore, this release marks the next phase in the deprecation of Intel chip-based Apple targets. macosX64, iosX64, tvosX64, and watchosX64 targets are now demoted to support-tier 3. This means they are no longer guaranteed to be tested on CI, and source/binary compatibility may not be maintained across compiler releases. Support for x86_64 Apple targets is slated for removal in Kotlin 2.4.0.

Kotlin/Wasm Innovations

Kotlin 2.3.0 brings significant advancements for Kotlin/Wasm, including fully qualified names by default, a new exception handling proposal for wasmWasi, and efficient compact storage for Latin-1 characters.

Fully Qualified Names Enabled by Default

In Kotlin 2.3.0, fully qualified names (FQNs) for Kotlin/Wasm targets are now enabled by default at runtime. Previously, developers had to manually enable support for the KClass.qualifiedName property to access FQNs, which was crucial for code ported from JVM or libraries expecting full names. This default enablement improves code portability and provides more informative runtime errors. Notably, this change does not increase the size of the compiled Wasm binary, thanks to optimizations for Latin-1 string literal storage.

Compact Storage for Latin-1 Characters

Kotlin 2.3.0 introduces an optimization for Kotlin/Wasm that stores string literals composed solely of Latin-1 characters in UTF-8 format, rather than the previous UTF-16 encoding. This significantly reduces metadata and results in:

  • Up to 13% smaller Wasm binaries compared to builds without this optimization.
  • Up to 8% smaller Wasm binaries even with fully qualified names enabled, compared to earlier versions.

This default enhancement is vital for web environments, improving download and startup times, and was a prerequisite for enabling fully qualified names by default.

New Exception Handling Proposal for wasmWasi

The new WebAssembly exception handling proposal is now enabled by default for the wasmWasi target in Kotlin 2.3.0. This aligns Kotlin/Wasm with modern WebAssembly runtimes, which are increasingly adopting this updated proposal. This change is applied to wasmWasi first due to its more controlled runtime environments, mitigating compatibility risks. For the wasmJs target, the new proposal remains off by default but can be manually enabled with the -Xwasm-use-new-exception-proposal compiler option.

Kotlin/JS Improvements

Kotlin 2.3.0 introduces experimental features for exporting suspend functions and representing LongArray with BigInt64Array. It also standardizes companion object access in interfaces, supports @JsStatic and @JsQualifier annotations in new contexts, and enables JavaScript default exports.

Experimental Suspend Function Export with @JsExport

Kotlin 2.3.0 introduces experimental support for directly exporting suspend functions to JavaScript using the @JsExport annotation. This eliminates the need for manual wrapping, significantly reducing boilerplate and enhancing interoperability between Kotlin/JS and JavaScript/TypeScript. Kotlin's async functions can now be called and overridden directly from JS/TS.

To enable this feature, add the following compiler option to your build.gradle.kts file:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xenable-suspend-function-exporting")
    }
}

Once enabled, suspend functions within @JsExport marked classes or functions can be consumed as regular JavaScript async functions:

Kotlin:

@JsExport
open class Foo {
    suspend fun foo() = "Foo"
}

JavaScript/TypeScript:

class Bar extends Foo {
    override async foo(): Promise<string> {
        return "Bar"
    }
}

BigInt64Array Representation for Kotlin's LongArray

Kotlin 2.3.0 introduces experimental support for representing Kotlin's LongArray type using JavaScript's native BigInt64Array. This improves interoperability with JavaScript APIs that expect typed arrays, offering a more natural export of LongArray-related APIs from Kotlin to JavaScript.

To enable this feature, add the following compiler option to your build.gradle.kts file:

kotlin {
    js {
        // ...
        compilerOptions {
            freeCompilerArgs.add("-Xes-long-as-bigint")
        }
    }
}

Unified Companion Object Access Across JS Module Systems

Kotlin 2.3.0 unifies the access pattern for companion objects within exported Kotlin interfaces across all JavaScript module systems (ES modules, CommonJS, AMD, UMD, and no modules). Previously, the access syntax varied, causing inconsistencies.

Now, companion objects in interfaces are accessed consistently, similar to companion objects in classes:

Kotlin:

@JsExport
interface Foo {
    companion object {
        fun bar() = "OK"
    }
}

JavaScript (all module systems):

Foo.Companion.bar()

This unification also extends to collection factory functions, ensuring consistent access across module systems (e.g., KtList.fromJsArray([1, 2, 3])). This feature is enabled by default, reducing interoperability issues and improving developer experience.

@JsStatic Annotation Support in Interfaces with Companion Objects

Kotlin 2.3.0 now supports the @JsStatic annotation within exported interfaces that contain companion objects. Previously, this was restricted to class companion objects.

This enables direct calls to static methods on interfaces from JavaScript, aligning the behavior with classes and simplifying API consumption:

Kotlin:

@JsExport
interface Foo {
    companion object {
        @JsStatic
        fun bar() = "OK"
    }
}

JavaScript (all module systems):

Foo.bar()

This feature is enabled by default, allowing static factory methods on interfaces and eliminating inconsistencies.

@JsQualifier Annotation in Individual Functions and Classes

In Kotlin 2.3.0, the @JsQualifier annotation can now be applied directly to individual functions and classes, mirroring the behavior of @JsModule and @JsNonModule. This removes the previous restriction of applying it only at the file level, allowing external JavaScript declarations to coexist with regular Kotlin declarations in the same file.

Kotlin:

@JsQualifier("jsPackage")
private external fun jsFun()

This default-enabled feature simplifies Kotlin/JS interop and promotes cleaner project structures.

JavaScript Default Exports

Kotlin/JS 2.3.0 introduces direct support for JavaScript default exports via the new @JsExport.Default annotation. Previously, only named exports were generated, often requiring workarounds for default exports.

When applied to a Kotlin declaration (class, object, function, or property), @JsExport.Default automatically generates an export default statement for ES modules (e.g., export default HelloWorker;). For other module systems, it functions like the regular @JsExport.

This default-enabled feature allows Kotlin code to better conform to JavaScript conventions, which is crucial for platforms like Cloudflare Workers and frameworks such as React.lazy.

Gradle Enhancements

Kotlin 2.3.0 ensures full compatibility with Gradle versions 7.6.3 through 9.0.0. While newer Gradle releases may also work, they might introduce deprecation warnings or limit functionality. The minimum supported Android Gradle Plugin (AGP) version is now 8.2.2, with a maximum supported version of 8.13.0. This release also features a new API for registering generated sources in Gradle projects.

New API for Registering Generated Sources

Kotlin 2.3.0 introduces an experimental API within the KotlinSourceSet interface for registering generated sources in Gradle projects. This API improves the developer experience by enabling IDEs (with upcoming IntelliJ IDEA support) to differentiate generated code from regular source files, providing distinct UI highlighting and triggering generation tasks upon project import. It is particularly beneficial for third-party code generation tools like KSP (Kotlin Symbol Processing).

Standard Library Updates

Kotlin 2.3.0 stabilizes the kotlin.time.Clock and kotlin.time.Instant time tracking functionalities and introduces significant improvements to the experimental UUID API.

UUID support in the standard library is currently experimental but slated for future stabilization. To opt in, use the @OptIn(ExperimentalUuidApi::class) annotation or add the following compiler option to your build file:

Gradle (Kotlin DSL):

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-opt-in=kotlin.uuid.ExperimentalUuidApi")
    }
}

Maven:

<build>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <configuration>
                <args>
                    <arg>-opt-in=kotlin.uuid.ExperimentalUuidApi</arg>
                </args>
            </configuration>
        </plugin>
    </plugins>
</build>

Improved UUID Generation and Parsing

The UUID API in Kotlin 2.3.0 receives several enhancements:

  • Graceful Parsing: New functions now return null when parsing invalid UUID strings, instead of throwing exceptions.
  • New Generators: Functions for generating v4 and v7 UUIDs.
  • Timestamp-Specific v7 UUIDs: Support for generating v7 UUIDs tied to specific timestamps.

Parsing Invalid UUIDs with null Return

New functions have been added to the Uuid API that return null for invalid UUID strings, enhancing error handling without exceptions:

  • Uuid.parseOrNull(): Parses UUIDs in hex-and-dash or hexadecimal format.
  • Uuid.parseHexDashOrNull(): Parses UUIDs strictly in hex-and-dash format.
  • Uuid.parseHexOrNull(): Parses UUIDs strictly in plain hexadecimal format.

Example:

import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

@OptIn(ExperimentalUuidApi::class)
fun main() {
    val valid = Uuid.parseOrNull("550e8400-e29b-41d4-a716-446655440000")
    println(valid) // 550e8400-e29b-41d4-a716-446655440000

    val invalid = Uuid.parseOrNull("not-a-uuid")
    println(invalid) // null

    val hexDashValid = Uuid.parseHexDashOrNull("550e8400-e29b-41d4-a716-446655440000")
    println(hexDashValid) // 550e8400-e29b-41d4-a716-446655440000

    val hexDashInvalid = Uuid.parseHexDashOrNull("550e8400e29b41d4a716446655440000")
    println(hexDashInvalid) // null
}

Generating v4 and v7 UUIDs

Two new functions, Uuid.generateV4() and Uuid.generateV7(), are introduced for explicit UUID generation. Uuid.random() continues to generate v4 UUIDs, similar to Uuid.generateV4().

Example:

import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

@OptIn(ExperimentalUuidApi::class)
fun main() {
    // Generates a v4 UUID
    val v4 = Uuid.generateV4()
    println(v4)

    // Generates a v7 UUID
    val v7 = Uuid.generateV7()
    println(v7)

    // Generates a v4 UUID (same as Uuid.generateV4())
    val random = Uuid.random()
    println(random)
}

Timestamp-Specific v7 UUID Generation

The new Uuid.generateV7NonMonotonicAt() function allows generating v7 UUIDs for specific moments in time. Unlike Uuid.generateV7(), this function does not guarantee monotonic ordering for multiple UUIDs created at the same timestamp. It is useful for identifiers tied to known historical timestamps, such as recreating event IDs.

Example:

import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import kotlin.time.ExperimentalTime
import kotlin.time.Instant

@OptIn(ExperimentalUuidApi::class, ExperimentalTime::class)
fun main() {
    val timestamp = Instant.fromEpochMilliseconds(1577836800000) // 2020-01-01T00:00:00Z

    // Generates a v7 UUID for the specified timestamp (non-monotonic)
    val v7AtTimestamp = Uuid.generateV7NonMonotonicAt(timestamp)
    println(v7AtTimestamp)
}

Compose Compiler: Stack Traces for Minified Android Apps

Kotlin 2.3.0 extends the experimental Compose stack traces feature to minified Android applications by outputting ProGuard mappings. This allows identifying composable functions in release builds without runtime source information overhead, using "group keys". This feature requires Compose runtime 1.10 or newer.

To enable group key stack traces, add the following line before initializing any @Composable content:

Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.GroupKeys)

When enabled, the Compose runtime appends its own stack trace after a crash during composition, measure, or draw passes, even in minified apps:

java.lang.IllegalStateException: <message> at <original trace> Suppressed: androidx.compose.runtime.DiagnosticComposeException: Composition stack when thrown: at $$compose.m$123(SourceFile:1) at $$compose.m$234(SourceFile:1) ...

The Compose Compiler Gradle plugin now appends group key entries to R8-produced ProGuard mapping files, deobfuscating these stack traces. Note that mapping file generation for group key stack traces only occurs when R8 is enabled. If these tasks cause build issues, the feature can be disabled:

composeCompiler {
    includeComposeMappingFile.set(false)
}

A known issue (KT-83099) exists where some code from Android Gradle Plugin-supplied project files may not appear in stack traces.

Breaking Changes and Deprecations

Kotlin 2.3.0 introduces several important breaking changes and deprecations:

  • Language Version Support: The compiler no longer supports -language-version=1.8. For non-JVM platforms, -language-version=1.9 is also no longer supported. While feature sets older than 2.0 (except 1.9 for JVM) are not supported, the language remains fully backward-compatible with Kotlin 1.0.
  • Gradle Plugin Compatibility: If kotlin-dsl and kotlin("jvm") are used together, a warning about unsupported Kotlin plugin versions may appear.
  • Kotlin Multiplatform Android Target: Support for the Android target in Kotlin Multiplatform is now provided via Google's com.android.kotlin.multiplatform.library plugin. Projects must migrate to this new plugin and rename androidTarget blocks to android. Using the Kotlin Multiplatform Gradle plugin with androidTarget alongside AGP 9.0.0+ will result in a configuration error.
  • Android Gradle Plugin 9.0.0+ and kotlin-android: AGP 9.0.0 and later versions have built-in Kotlin support, making the kotlin-android plugin redundant. Using kotlin-android with AGP 9.0.0+ will cause a configuration error; older AGP versions will show a deprecation warning.
  • Ant Build System: Support for the Ant build system has been removed.

How to Update

To update your projects to Kotlin 2.3.0, simply change the Kotlin version to 2.3.0 in your build scripts. The necessary Kotlin plugins are bundled with the latest versions of IntelliJ IDEA and Android Studio.