Kotlin 2.4.20-Beta2 dropped quietly in the EAP channel this week, and if you're running any KMP code that touches iOS, there are two things in here that will materially change how you work. Sealed class export to Swift enums finally makes exhaustive pattern matching across the Kotlin–Swift boundary possible without workarounds. And incremental klib compilation — which has been sitting behind an opt-in flag for a while — is now on by default, which means faster iterative builds for Kotlin/Native targets at zero configuration cost.
This is a .20 minor release on top of 2.4.0 stable, so there's no dramatic pitch to make. But the sealed-class story specifically has been one of the more requested KMP improvements, and the implementation here is clean enough that it's worth understanding before it lands in stable.
1. Sealed Classes Export as Swift Enums
The problem before this: when Kotlin sealed classes crossed into Swift via the existing Swift Export mechanism, the iOS side saw a flat class hierarchy with no exhaustive matching. You could check types manually, but there was no way for the Swift compiler to tell you that you'd missed a case — the whole point of sealed types was lost at the boundary.
2.4.20-Beta2 changes the generated output. Sealed classes and sealed interfaces now map to Swift enums, with each subtype becoming a case. The Kotlin runtime generates a .sealedType() method on each sealed value that returns the enum case, and Swift's switch can exhaustively match it without a default branch.
Here's a concrete example that shows why this matters. Take a standard result type sealed hierarchy:
// Kotlin — shared module
sealed interface ApiResult
class Success(val data: String) : ApiResult
class Failure(val code: Int, val message: String) : ApiResult
class Loading : ApiResult
fun fetchUserProfile(): ApiResult = Success("Vikas")
On the Swift side before this change, you'd be writing ugly is checks or using as? casts, with no compiler enforcement that you handled every case. With 2.4.20 Swift Export, the generated Swift lets you write:
// Swift — iOS app
let result = fetchUserProfile()
let message = switch result.sealedType() {
case let .success(type):
"Got data: \(type.value.data)"
case let .failure(type):
"Error \(type.value.code): \(type.value.message)"
case .loading:
"Fetching..."
}
// Compiler errors if you miss a case — no `default` needed
This is a meaningful ergonomics improvement for any KMP architecture that uses sealed classes as the return type from shared business logic — which is basically every production KMP codebase. The standard pattern of having a shared UiState sealed hierarchy in the domain layer and observing it on both Android (via when) and iOS (via switch) now has the same safety guarantees on both sides.
What this doesn't cover yet
The exhaustive switch works for direct sealed subtypes. Nested sealed hierarchies and sealed classes with generic type parameters have limitations in the current Beta2 — check the Kotlin EAP docs for the exact edge cases before relying on this in a production KMP module. For simple one-level sealed hierarchies, it works cleanly.
2. Cross-Language Inheritance: Swift Implementing Kotlin Interfaces
The companion feature in the same Beta2 drop: the "reverse import" pattern where a Swift class implements a Kotlin interface and passes itself back to Kotlin code. This unlocks a pattern that's been painful for years in KMP — using a platform-native Swift library to provide an implementation of a shared interface without wrapping everything in a bridge adapter.
// Kotlin — shared module
interface Analytics {
fun track(event: String, properties: Map<String, String>)
}
fun initializeApp(analytics: Analytics) { /* ... */ }
// Swift — iOS app, using platform-native Mixpanel SDK directly
class MixpanelAnalytics: KotlinBase & Analytics {
func track(event: String, properties: [String: String]) {
Mixpanel.mainInstance().track(event: event, properties: properties)
}
}
initializeApp(analytics: MixpanelAnalytics())
Before this, the workaround was either a Kotlin expect/actual declaration (which still required a Kotlin wrapper on the iOS side) or a manual adapter class that translated between Kotlin interfaces and Swift protocols. Neither is wrong exactly, but the new direct inheritance feels like how it should have worked from the start.
Practical impact: this is most useful when the platform library you want to use on iOS has no Kotlin/JVM equivalent — analytics SDKs, device-specific security APIs, ARKit/RealityKit integrations. You define the contract in Kotlin, implement it in Swift using the native SDK directly, and pass it through. No adaptation layer.
3. Incremental klib Compilation Is Now the Default
If you've been building Kotlin/Native targets — meaning any KMP code that compiles to iOS, macOS, or tvOS — you've probably noticed that Kotlin/Native debug builds are slower than the equivalent JVM compilation. A big chunk of that overhead comes from recompiling klib artifacts from scratch on every change, even when only a small part of the module changed.
Incremental klib compilation was introduced as an opt-in feature in an earlier 2.4.x release. Beta2 makes it the default. You don't change anything — it just kicks in. The compiler tracks which parts of a klib have changed and only recompiles those sections, leaving the rest of the artifact intact.
The JetBrains team reports meaningful reductions in incremental build times for Kotlin/Native targets. Clean builds may be slightly slower (the compiler does more bookkeeping upfront), but the incremental path — which is what you hit on every code change during development — is significantly faster.
If you need to disable it for any reason — say, you're hitting a correctness bug with the incremental logic — add kotlin.incremental.native=false to your gradle.properties. But the expectation is that this is stable enough to be on by default for the 2.4.20 final release.
4. Four New Collection Functions in stdlib
These are small but genuinely handy. The stdlib gains four new extension functions for collections, sequences, and arrays. All four are experimental in Beta2, requiring @OptIn(ExperimentalStdlibApi::class).
Collection.allDistinct()— returnstrueif all elements are unique by equalityCollection.allDistinctBy(selector)— unique by a projected propertyCollection.allEqual()— returnstrueif all elements are the same valueCollection.allEqualBy(selector)— equal by a projected property
The practical use cases I reach for immediately: validating that a list of items has no duplicate IDs (items.allDistinctBy { it.id }), checking that a group of responses all have the same status, or asserting uniqueness constraints before persisting to Room. You could build these with distinctBy().size == size or toSet().size combinations, but having named functions makes the intent readable in a way that the manual version doesn't.
@OptIn(ExperimentalStdlibApi::class)
fun validatePlaylistItems(items: List<MediaItem>): Boolean {
// Ensure no duplicate media IDs before adding to ExoPlayer playlist
return items.allDistinctBy { it.mediaId }
}
fun isConsistentSyncBatch(events: List<SyncEvent>): Boolean {
// All events in a batch should target the same userId
return events.allEqualBy { it.userId }
}
5. Kotlin Compiler Native Image (Experimental)
This one is worth knowing about even if you won't use it immediately. JetBrains is distributing an experimental build of kotlinc as a GraalVM native image — a self-contained binary that doesn't require a JVM to start. The startup time improvement over the JVM-based compiler is significant, which matters most for CI environments that pay the JVM cold-start cost on every invocation and for editor tooling that runs the compiler frequently.
The native image bundles the Serialization, Compose compiler, All-open, no-arg, SAM with receiver, Assignment, Lombok, and Power-assert plugins. For most Android/KMP codebases, that's the full plugin set you'd need. It's available as a download from the v2.4.20-Beta2 GitHub release tag.
This is explicitly experimental — don't swap it into your production build pipeline yet. But for local development on CI-heavy workflows, it's worth benchmarking.
Other Changes Worth Noting
StackTraceRecoverable interface lands in kotlin.coroutines. If you have custom exception types that you pass through coroutine boundaries, implementing StackTraceRecoverable<T> and providing a copyForStackTraceRecovery() method gives kotlinx.coroutines a way to preserve the stack trace across suspension points. This is experimental and JVM-only for now, but useful for custom exception hierarchies in complex async flows.
Kotlin/JS suspend lambda export adds -Xsuspend-lambda-exporting to export suspend lambdas as JavaScript async functions — useful if you have a KMP module targeting both Android and a web frontend via Kotlin/JS.
Kotlin/Wasm now supports Wasmtime as a standalone runtime via wasmtime() in the Kotlin DSL. This is relevant for server-side Wasm use cases more than mobile, but it's a good sign for the Kotlin/Wasm ecosystem maturing.
Should You Adopt This Now?
Split it into three decisions.
Incremental klib compilation: you get this for free just by staying on Kotlin 2.4.x and upgrading to 2.4.20 when it goes stable. No API changes, no configuration. Run it.
Sealed class Swift Export and cross-language inheritance: if you have a KMP module targeting iOS, prototype these in Beta2 now. They're the kind of ergonomic improvements that fundamentally change how you structure shared code — sealed return types from shared use cases, platform implementations of shared interfaces. The sooner you have a feel for the edge cases, the smoother the stable adoption will be. Don't ship them to production on Beta2, but build something real with them.
New stdlib functions and StackTraceRecoverable: these require @OptIn in Beta2, which the stable release will likely remove. Fine to use in new code if you're targeting internal tooling. For shipped app code, wait for stable.
The through-line across this release is KMP interop becoming less of a "Kotlin devs writing Android-flavored Swift" problem and more of an actual bidirectional type system. Sealed types preserving their safety guarantees across the boundary, Swift implementing Kotlin interfaces directly — these are the mechanical changes that make KMP feel like a genuine multi-platform architecture rather than a shared business logic hack. That's a slow shift, but 2.4.20 is a meaningful step in it.
No comments yet. Be the first to leave one!