All Articles
Reading Time
14 min read
Published
2026-07-03
Word Count
3,449words

Grab a coffee — this one is a deep dive!

Kotlin 2.4.0: Context Parameters and Swift Package Support

Summary

Kotlin 2.4.0 makes context parameters and explicit backing fields Stable, adds Java 26 support, brings Swift package dependencies to KMP. Differences from the experimental state, with code examples.

  • Kotlin 2.4.0 (June 3, 2026) makes context parameters and explicit backing fields Stable.
  • Explicit context argument passing and callable references with context parameters remain Experimental.
  • KMP projects can now declare a Swift package dependency directly in Gradle via swiftPMDependencies.
  • Gradle 7.6.3–9.5.0 and minimum AGP 8.5.2 support arrive along with Java 26 bytecode generation.
Kotlin 2.4.0: Context Parameters and Swift Package Support

Kotlin 2.4.0 was released on June 3, 2026, moving two features that stayed experimental across the last two releases — context parameters and explicit backing fields — to Stable. On the JVM side, Java 26 bytecode support arrived; on the Multiplatform side, you can now declare a Swift Package dependency directly through Gradle. In this post you'll see, line by line, what changed from context parameters' experimental state, real code patterns, and how to wire a Swift package dependency into Gradle in KMP projects.

💡 Pro Tip: When moving to context parameters, start by moving only the dependencies you'd otherwise pass by hand on every call and that rarely change (logger, UserService, and the like) into context parameters — putting frequently changing state into a context parameter makes the code harder to read.

Table of Contents

Kotlin from 2.1 to 2.4 in short

Kotlin 2.4.0 is, according to the official announcement, a language release that makes context parameters and explicit backing fields Stable, adds Java 26 support on the JVM, and moves Swift package dependencies and Swift export to Alpha on the Native side. Per the releases.html page, the release date is June 3, 2026.

Context parameters, introduced as Experimental in Kotlin 2.2.0 and 2.3.0, along with the @all meta-target, annotation use-site defaults, and explicit backing fields — all four of these features became Stable with 2.4.0. In other words, features you spent two releases trying behind opt-in flags like -Xcontext-parameters can now be used in production code without any extra flag.

In the same release, the kotlin.uuid.Uuid API in the standard library also became Stable; we'll cover that in a separate section.

If you already read our earlier post Kotlin 2.1: K2 Compiler and Context Parameters, you already know context parameters' experimental state — the real news in 2.4.0 is that this API can now be used in production code without an -X flag. What you tried back in the 2.1 era, though, was context receivers: per the official docs, context parameters replace that older experimental feature, and you access members via a named parameter instead of an implicit receiver. This syntax, introduced in 2.2.0, hasn't changed through 2.4.0; what changed is that the compiler now accepts it by default. For library authors this distinction matters: putting an experimental API on your public surface (breaking-change risk) is very different from putting a Stable one there.

Context parameters stable: differences from the experimental era

What's Stable is context parameter definition and implicit resolution at the call site. What's still experimental is passing context arguments explicitly at the call site (behind -Xexplicit-context-arguments) and callable references with context parameters.

Keep this distinction clear: "context parameters is stable" doesn't mean every context-related feature is stable. Constraints still apply too:

  • Constructors cannot declare context parameters.
  • Properties with context parameters cannot use a backing field, an initializer, or delegation.

These constraints close the door on "using context parameters instead of constructor injection" without a DI framework — context parameters currently work only at the function and computed-property level, not in class construction. Missing this can lead an experienced DI user to look for context parameters in the wrong place and get stuck wondering "why doesn't this work in the constructor."

The table below summarizes what changed from the experimental era (2.2/2.3) to today:

Feature
2.2.0 / 2.3.0 status
2.4.0 status
Context parameter definition (context(x: T) fun ...)
Experimental (-Xcontext-parameters)
Stable
Implicit resolution at the call site
Experimental
Stable
Passing explicit context arguments
Experimental
Experimental (unchanged)
Callable references with context parameters
Experimental
Experimental (unchanged)
@all meta-target
Experimental
Stable
Annotation use-site default
Experimental
Stable

Real usage patterns (DI, logging, scope)

The core usage pattern of context parameters is to make a shared dependency, like a service or logger, accessible in a type-safe way without adding it as a parameter to every function signature:

kotlin
1interface UserService {
2 fun log(message: String)
3}
4 
5context(users: UserService)
6fun outputMessage(message: String) {
7 users.log(message)
8}

You don't need to pass the users parameter by hand — if a UserService instance exists in context at the call site, the compiler finds it automatically. Compare this to the classic approach: as a regular parameter (fun outputMessage(users: UserService, message: String)), every caller would have to carry the users value by hand — and in layered architectures with deep call chains (repository → use case → view model), every intermediate layer would carry a parameter it has no interest in, just to get it to a lower layer. A context parameter hands this "carrying" burden off to the compiler.

You can also access it without naming it, using an anonymous context parameter (_) or contextOf<T>():

kotlin
1context(_: UserService)
2fun quickLog(message: String) {
3 contextOf<UserService>().log(message)
4}

If there's more than one matching context value, the compiler gives you an ambiguity error. This can be especially annoying when overloads differ only by context parameter type — say you have two sendNotification() functions, one with an EmailSender context parameter and one with an SmsSender context parameter, and both are present at the call site: the compiler can't know which you meant. In that case you need the experimental explicit context argument syntax to state which context value to use.

In practice, you get the most benefit from context parameters across layers, with services you don't want to pass by hand to every function but do want to mock in tests: logger, clock, feature-flag reader, and the like. Making these dependencies context parameters documents what a function needs without cluttering its signature — anyone reading context(users: UserService) understands this function needs a UserService, but that it isn't the "actual work" parameter.

To write the same function during the experimental era (2.2/2.3), you needed to add the -Xcontext-parameters compiler flag to your build script:

kotlin
1// build.gradle.kts — required during the 2.2.0 / 2.3.0 era
2tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
3 compilerOptions {
4 freeCompilerArgs.add("-Xcontext-parameters")
5 }
6}

After upgrading to Kotlin 2.4.0 you can remove this block — context parameter definition and implicit resolution at the call site are now part of the compiler's default behavior. If you forget and leave this flag in your CI after upgrading, the compiler won't throw an error, you'll just have a redundant line; cleaning it up still keeps your build script current.

Java 26 support and toolchain

The Kotlin 2.4.0 compiler can now produce Java 26 bytecode. In the same release, annotation-in-metadata support for the Kotlin Metadata JVM library was turned on by default; this lets annotation processors and other tools read annotations at the metadata level without reflection or source-code changes.

On the toolchain side, Gradle 7.6.3 through 9.5.0 is fully supported; the minimum AGP (Android Gradle Plugin) version rose to 8.5.2. Check these thresholds before upgrading — see the detailed table in "Upgrade checklist" below.

Java 26 bytecode support matters especially for server-side Kotlin projects targeting the JVM (Ktor, Spring Boot with Kotlin): moving your jvmTarget to Java 26 is a prerequisite for teams that want the JVM-level improvements (garbage collector, class loading) that version brings. Annotation-in-metadata being on by default is more relevant to library and framework authors — a KSP-based annotation processor can now read annotation info directly from Kotlin metadata without reflection or source changes.

Think about these two changes (Java 26 bytecode + annotation-in-metadata) separately: one concerns your target JVM version (jvmTarget), the other the compile-time annotation mechanism. If you run an Android project, Java 26 bytecode support may not concern you directly — the runtime uses its own bytecode target — but on the JVM (server-side Kotlin, a desktop Compose app), this distinction affects your upgrade decision.

KMP: using Swift packages as a dependency

The most practical Native-side novelty in Kotlin 2.4.0: Kotlin Multiplatform projects can now declare Swift Packages as a dependency directly through Gradle for the iOS app target. You declare a Swift package with the swiftPMDependencies block, along with a URL, a version, and the products you want to use:

kotlin
1kotlin {
2 swiftPMDependencies {
3 swiftPackage(
4 url = url("https://github.com/firebase/firebase-ios-sdk.git"),
5 version = from("12.11.0"),
6 products = listOf(
7 product("FirebaseAI"),
8 product("FirebaseAnalytics")
9 )
10 )
11 }
12}

Per the official docs, SwiftPM import brings Objective-C-visible APIs — from Objective-C and Swift code — over to the Kotlin side; pure Swift APIs don't come through this route. The same doc states the feature is Alpha, and that exporting a KMP module using SwiftPM import as a Swift package isn't supported yet. For CocoaPods projects, an official migration guide has also been published, offering a step-by-step roadmap for moving to SwiftPM-based dependency management.

This feature reduces the dependency on CocoaPods especially for teams that want to bring Firebase, analytics SDKs, or non-first-party native iOS libraries into a KMP project.

In practice, this lets a KMP team run iOS-side dependency management through a single Gradle build file, instead of two separate tools (CocoaPods + Gradle). Listing only the products you actually need in products keeps the linked code scoped — no need to add every product of a package one by one, just the module you actually use, like FirebaseAnalytics.

If you still use CocoaPods, deciding with your team which dependencies to move first, before applying the migration guide, reduces the risk of changing all dependency management in one PR on a large KMP codebase.

If you're building a modular KMP project, pay attention to which module you define Swift package dependencies in too — the module-boundary principles from our modular architecture and advanced SPM usage posts apply here as well: bind a platform-specific dependency to the relevant platform source set, not to the shared commonMain module.

Where Swift export stands today

Brief context: with Kotlin 2.4.0, Swift export officially moved to Alpha; suspend functions can export to Swift's async equivalent, and kotlinx.coroutines Flows can export to AsyncSequence. This works in the opposite direction from the Swift package dependency feature above: one lets Kotlin code be consumed by Swift (export), the other lets Swift packages be consumed by Kotlin/Native (import). Don't mix up the two flows — an in-depth treatment of Swift export and the future of KMP's Objective-C bridge is a separate topic, outside this post's scope.

In practice, Alpha means the API isn't stable yet, can change between releases, and you need to check behavior changes at every upgrade before using it in production-critical flows. The SwiftPM import feature above is Alpha per its docs too — one import-direction, one export-direction — so evaluate both with the same caution.

UUID API and explicit backing fields

The kotlin.uuid.Uuid API became Stable in 2.4.0; the functions that explicitly pick a version — Uuid.generateV4(), Uuid.generateV7(), and Uuid.generateV7NonMonotonicAt() — remain Experimental. Uuid.random(), which generates a random V4, is Stable and doesn't require @OptIn:

kotlin
1import kotlin.uuid.Uuid
2import kotlin.uuid.ExperimentalUuidApi
3 
4fun createUser(): String {
5 val id = Uuid.random() // Stable: random V4
6 return id.toString()
7}
8 
9@OptIn(ExperimentalUuidApi::class)
10fun createOrderedId(): Uuid {
11 return Uuid.generateV7() // V7 Experimental: @OptIn required
12}

Explicit backing fields also became Stable this release — a feature letting you declare a property's read (get()) type separately from its write (backing field) type:

kotlin
1class UserRepository {
2 val users: List<String>
3 field = mutableListOf()
4 
5 fun addUser(user: String) {
6 users.add(user) // smart-cast to MutableList<String> inside the class
7 }
8}

This collapses the "read-only exposure" pattern — previously a separate private val + public val get() pair — into a single property declaration. The difference is clearer next to the old pattern:

kotlin
1// Old pattern: two separate properties + manual synchronization
2class UserRepositoryOld {
3 private val _users: MutableList<String> = mutableListOf()
4 val users: List<String>
5 get() = _users
6}

Both expose a read-only users of type List<String> externally; the difference is that explicit backing fields need no separate _users property or get(). You declare the read type (List<String>), and the compiler derives the backing field type (MutableList<String>) from the initializer — or write it explicitly with field: MutableList<String> = mutableListOf(). Because users is smart-cast to the backing field type inside the class, users.add(...) works there, while it's read-only from outside.

Upgrade checklist (Gradle 9.5.0)

Kotlin version upgrades usually go smoothly at the compiler level, but when toolchain version thresholds (Gradle, AGP, minimum platform versions) are skipped, you can hit unexpected build failures in CI. Version thresholds to check before upgrading your project to 2.4.0:

Component
Required minimum / range
Note
Gradle
7.6.3 – 9.5.0
Newer Gradle versions may also work; deprecation warnings or some new Gradle features may behave unexpectedly
AGP (Android Gradle Plugin)
8.5.2+
Minimum version raised
Apple targets (iOS/tvOS)
15.0+
Previous minimum was 14.0
Apple targets (macOS)
12.0+
Previous minimum was 11.0
Apple targets (watchOS)
8.0+
Previous minimum was 7.0

There's also a behavior change in module naming: the default cross-platform module name is now in {group}:{project_name} format. On the JVM you can opt out and revert to the old behavior inside build.gradle.kts.

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

Reader Reward

I put together a short checklist you can follow before upgrading to Kotlin 2.4.0, sequencing the items covered in this post. Follow the order below if you want to make the upgrade without breaking CI.

FAQ

What's new in Kotlin 2.4.0?

Kotlin 2.4.0 makes context parameters, explicit backing fields, and annotation use-site targets Stable; it stabilizes the UUID API; and it brings Java 26 bytecode support on the JVM and Swift package dependency support on Native. Swift export also moved to Alpha, and Gradle 9.5.0 compatibility was secured. Each item is covered in its own section with code examples — context parameters and Swift package dependencies directly affect day-to-day development workflow.

What are context parameters, and how do they differ from context receivers?

Context parameters let functions and properties declare dependencies (context(users: UserService) fun ...) that are implicitly available in the surrounding context — shared, rarely changing values, like a service, used without passing them by hand. It replaces the older "context receivers" proposal, has named-parameter syntax, and does type-based resolution; with 2.4.0 it became Stable except for context argument passing and callable reference support.

How do you add a Swift package as a dependency in Kotlin Multiplatform?

You declare it in your Gradle build file with the kotlin { swiftPMDependencies { swiftPackage(url = ..., version = from("x.y.z"), products = listOf(product("ProductName"))) } } block. An official migration guide is also available for projects moving from CocoaPods.

Which Gradle version does Kotlin 2.4 require?

Per the official announcement, Gradle 7.6.3 through 9.5.0 is fully supported. Newer Gradle versions generally work too, but you may run into deprecation warnings or newer Gradle features that haven't been tested yet.

What is an explicit context argument, and why is it still experimental?

An explicit context argument is syntax that lets you state a context parameter explicitly when calling a function, instead of leaving it to implicit resolution. This call-site syntax was introduced as Experimental in 2.4.0 and requires opt-in via the -Xexplicit-context-arguments flag; details are tracked in the feature's KEEP document.

How do you use the UUID API?

The kotlin.uuid.Uuid class, along with the Uuid.random() call, works through a Stable API. The generation functions that explicitly pick the V4 and V7 formats remain Experimental APIs that require @OptIn(ExperimentalUuidApi::class).

What are explicit backing fields for?

Explicit backing fields let you separate the read type a property exposes externally (get()) from the write type it holds internally (the backing field) — for example, exposing a read-only List<String> externally while holding a MutableList<String> internally. Before 2.4.0, you needed a separate private property + public getter pair to do this; now you can do it in a single property declaration with field = ... syntax.

What's the difference between Swift export and a SwiftPM dependency?

They're two mechanisms that work in opposite directions. A SwiftPM dependency (this post's main topic) lets you import a Swift package into Kotlin/Native — via the swiftPMDependencies Gradle block. Swift export is the opposite: it lets you export Kotlin code (suspend functions, Flows) to Swift, and as of 2.4.0 it's Alpha. You can use both at once in a KMP project, but remember both are Alpha.

Conclusion

Kotlin 2.4.0 finally brings context parameters — experimental across two releases — into production, and on Native it opens an alternative to CocoaPods with Swift package dependencies. Before moving to context parameters, read our earlier post on the K2 compiler and context parameters' experimental state to see the difference between that era and this Stable one.

If you're setting up a modular structure in your KMP project, our posts on modular iOS architecture and Swift Package Manager and advanced modular architecture with SPM offer guidance on where you should wire Swift package dependencies. If you want to see the real-world experience of teams using KMP in production, take a look at the Kotlin Multiplatform 1.1 Stable production case study and Compose Multiplatform Android + iOS production deployment. If you're deciding on a shared-core architecture with Kotlin, our Swift Android SDK vs. KMP? comparison can also help with that decision.

In short: no need to rush into context parameters — Stable means libraries and large codebases can start using this API on their public surface, not that you need to rewrite existing code for this pattern right away. Swift package dependencies can be more urgent: if you're a KMP team looking to reduce CocoaPods maintenance, start evaluating this now, keeping its Alpha label in mind.

Sources

Tags

#Kotlin#KMP#Context Parameters#Swift Package Manager#Java 26#Gradle#Multiplatform
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share