PCSalt
YouTube GitHub
Back to Kotlin
Kotlin · 3 min read

Kotlin 2.x — What Changed and How to Migrate

Everything you need to know about migrating to Kotlin 2.x — the K2 compiler, breaking changes, new features, Compose compiler integration, and step-by-step migration guide.


Kotlin 2.0 shipped in May 2024 with the biggest change since Kotlin 1.0: a completely rewritten compiler. Kotlin 2.1 followed in November 2024 with refinements. If you’re still on Kotlin 1.9.x, here’s what changed and how to migrate.

The K2 compiler

The headline feature. Kotlin’s original compiler (the “old frontend”) was rewritten from scratch. The new one — K2 — is:

  • Up to 2x faster for compilation
  • More consistent type inference
  • Better error messages
  • Foundation for future features (context receivers, explicit backing fields)

K2 isn’t a new language — it compiles the same Kotlin code. But it does it faster and catches some bugs that the old compiler missed.

What K2 changes for you

Most code compiles identically. But K2 has stricter type checking in some edge cases:

// Old compiler: compiles
// K2: error — smart cast not possible because variable is mutable
var x: Any = "hello"
if (x is String) {
    println(x.length) // K2 might flag this if x could be reassigned
}

K2 is smarter about smart casts but also stricter. Code that relied on the old compiler’s quirks may need adjustment.

Compose compiler integration

Before Kotlin 2.0, the Compose compiler was a separate Gradle plugin:

// Old way
plugins {
    id("org.jetbrains.kotlin.plugin.compose") version "1.5.14"
}

In Kotlin 2.0+, the Compose compiler is integrated into the Kotlin compiler plugin:

// New way (Kotlin 2.0+)
plugins {
    kotlin("plugin.compose") version "2.1.0"
}

The Compose compiler version is now tied to the Kotlin version. No more version compatibility matrices. Update Kotlin → Compose compiler updates automatically.

New language features in 2.0

Guard conditions in when

sealed class Response {
    data class Success(val data: String) : Response()
    data class Error(val code: Int, val message: String) : Response()
}

fun handle(response: Response) {
    when (response) {
        is Response.Success -> println(response.data)
        is Response.Error if response.code == 404 -> println("Not found")
        is Response.Error if response.code >= 500 -> println("Server error")
        is Response.Error -> println("Error: ${response.message}")
    }
}

if guards on when branches let you add conditions without nesting.

Non-local break and continue in inline lambdas

listOf(1, 2, 3, 4, 5).forEach { value ->
    if (value == 3) return@forEach // was already possible (like continue)
}

// New in 2.1 — break from inline lambda
for (item in items) {
    item.children.forEach { child ->
        if (child.isTerminal) break // breaks the outer for loop
    }
}

Improved type inference

// Old compiler: needed explicit type
val list = buildList {
    add("hello")
    add(42) // old compiler: error
}

// K2: correctly infers List<Any>

K2 resolves more type inference scenarios without explicit type annotations.

New features in Kotlin 2.1

Improved exhaustive when with sealed types

K2 provides better exhaustiveness checking for sealed class hierarchies, including nested sealed classes.

Stable Gradle DSL for compiler options

kotlin {
    compilerOptions {
        apiVersion.set(KotlinVersion.KOTLIN_2_1)
        languageVersion.set(KotlinVersion.KOTLIN_2_1)
        freeCompilerArgs.add("-Xcontext-receivers")
    }
}

Replaces the old kotlinOptions block (which was Kotlin JVM-only and deprecated).

Breaking changes

1. Kotlin Gradle Plugin API changes

The kotlinOptions DSL in build.gradle.kts is deprecated:

// Deprecated
tasks.withType<KotlinCompile>().configureEach {
    kotlinOptions {
        jvmTarget = "21"
    }
}

// New way
kotlin {
    compilerOptions {
        jvmTarget.set(JvmTarget.JVM_21)
    }
}

2. Removed deprecated APIs

Several previously deprecated APIs were removed in 2.0:

  • kotlin.coroutines.experimental — use kotlinx.coroutines (already the standard since 2018)
  • Some JS-specific APIs
  • Internal compiler APIs that some libraries depended on

3. Stricter type checking

K2’s stricter type inference may flag code that previously compiled:

// Example: this compiled with old compiler but K2 rejects it
fun <T> generic(): T = TODO()
val x: String = generic() // K2 needs more context in some cases

Most real-world code is fine. But if you have complex generic code, test carefully.

4. Build script changes for AGP 9+

If you’re also updating Android Gradle Plugin to 9.x:

// AGP 9.0+ has Kotlin built-in — no separate kotlin-android plugin needed
plugins {
    id("com.android.application")
    // No kotlin("android") needed
}

Step-by-step migration

1. Update Kotlin version

In libs.versions.toml:

[versions]
kotlin = "2.1.0"

Or in build.gradle.kts:

plugins {
    kotlin("jvm") version "2.1.0"
}

2. Update Gradle plugin

Kotlin 2.x requires Gradle 7.6.3+ (Gradle 8.x or 9.x recommended):

# gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip

3. Replace kotlinOptions with compilerOptions

// Before
tasks.withType<KotlinCompile>().configureEach {
    kotlinOptions {
        jvmTarget = "21"
        freeCompilerArgs += listOf("-opt-in=kotlin.RequiresOptIn")
    }
}

// After
kotlin {
    compilerOptions {
        jvmTarget.set(JvmTarget.JVM_21)
        freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
    }
}

4. Update Compose compiler (if using Compose)

Remove the old Compose compiler plugin and use the new one:

plugins {
    kotlin("plugin.compose") version "2.1.0"
}

// Remove this:
// composeOptions {
//     kotlinCompilerExtensionVersion = "1.5.14"
// }

5. Update dependencies

Some libraries need minimum versions for Kotlin 2.x compatibility:

LibraryMinimum version for Kotlin 2.x
kotlinx-coroutines1.9.0+
kotlinx-serialization1.7.0+
Ktor3.0+
Arrow1.2.4+
Kotest5.9+
MockK1.13.12+

6. Build and fix

./gradlew build

Address compilation errors. Most will be:

  • Deprecated API usage → use the replacement
  • Stricter type inference → add explicit type annotations
  • Kotlin Gradle DSL changes → update build scripts

7. Run tests

./gradlew test

K2’s stricter checking might surface bugs that the old compiler hid — especially around nullability and type narrowing.

Should you migrate now?

Yes, if:

  • You’re starting a new project
  • You want faster build times
  • You need Compose compiler alignment (no more version matrices)
  • Your dependencies are up to date

Wait, if:

  • A critical dependency doesn’t support Kotlin 2.x yet
  • You’re in the middle of a major release
  • Your build uses compiler plugins that haven’t been updated for K2

Check your dependency compatibility before migrating. Most major libraries supported Kotlin 2.x within months of release.

Summary

Kotlin 2.x brings:

  • K2 compiler — up to 2x faster, stricter, better errors
  • Compose compiler integration — no more version matrices
  • Guard conditions in when — cleaner pattern matching
  • Improved type inference — fewer explicit type annotations
  • New Gradle DSLcompilerOptions replaces kotlinOptions

Migration is straightforward for most projects: update the version, fix deprecated API usage, and update build scripts. The K2 compiler improvements alone make it worth the effort.