Kotlin 2.2.21 dropped on October 23, 2025. I usually wait a week or two before upgrading, but this one has fixes that made me update immediately - especially the Parcelize issues that were breaking navigation in one of my apps.
Here's what's actually worth knowing about this release, with code examples from real use cases.
The Headlines
Compose Performance - About 30% better performance in lazy layouts. You'll notice this on mid-tier devices.
Parcelize Fixed - Version 2.2.20 broke Parcelize for complex objects. 2.2.21 fixes it. If you skipped 2.2.20, good call.
WebAssembly - Safari was crashing on exceptions in Kotlin/Wasm code. Fixed now.
Gradle Build Times - Configuration cache actually works now. My multi-module project went from 3+ minute builds to under 2 minutes.
The Compose Performance Improvement
If you have lazy layouts (LazyColumn, LazyRow, LazyVerticalGrid) with frequent state updates, you'll see smoother scrolling after upgrading. The K2 compiler generates more efficient recomposition logic now.
Here's a typical product catalog screen that benefits:
@Composable
fun ProductCatalogScreen(viewModel: ProductViewModel = viewModel()) {
val products by viewModel.products.collectAsState()
val selectedCategory by viewModel.selectedCategory.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
// Filter chips
LazyRow(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(Category.values()) { category ->
FilterChip(
selected = category == selectedCategory,
onClick = { viewModel.selectCategory(category) },
label = { Text(category.name) }
)
}
}
// Product grid - noticeably smoother in 2.2.21
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(items = products, key = { it.id }) { product ->
ProductCard(
product = product,
onAddToCart = { viewModel.addToCart(it) }
)
}
}
}
}
@Composable
fun ProductCard(product: Product, onAddToCart: (Product) -> Unit) {
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.animateItemPlacement()
.clickable { isExpanded = !isExpanded }
) {
Column {
AsyncImage(
model = product.imageUrl,
contentDescription = product.name,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentScale = ContentScale.Crop
)
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = product.name,
style = MaterialTheme.typography.titleSmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "$${product.price}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
IconButton(onClick = { onAddToCart(product) }) {
Icon(
imageVector = Icons.Default.ShoppingCart,
contentDescription = "Add to cart"
)
}
}
if (isExpanded) {
Text(
text = product.description,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
}
}
Before 2.2.21, scrolling through 100+ items with animations would drop frames on phones like the Pixel 4a. Now it stays at 60fps. Not a dramatic visual change, but it feels noticeably smoother.
Parcelize Works Again
If you were on Kotlin 2.2.20 and passing complex Parcelable objects between screens, you probably hit issues. I had crashes in production when navigating to a profile editor screen. The fix was downgrading to 2.2.0 until this patch came out.
Here's the kind of data class that was breaking:
@Parcelize
data class UserProfile(
val userId: String,
val username: String,
val displayName: String,
val bio: String,
val profileImageUrl: String,
val followers: Int,
val following: Int,
val posts: List<Post>,
val settings: UserSettings
) : Parcelable
@Parcelize
data class Post(
val postId: String,
val imageUrl: String,
val caption: String,
val likes: Int,
val comments: Int,
val timestamp: Long,
val location: String?
) : Parcelable
@Parcelize
data class UserSettings(
val isPrivate: Boolean,
val allowComments: Boolean,
val notificationsEnabled: Boolean,
val theme: AppTheme,
val language: String
) : Parcelable
enum class AppTheme { Light, Dark, Auto }
Passing this through Navigation arguments:
navController.currentBackStackEntry?.savedStateHandle?.set("user_profile", userProfile)
navController.navigate("editProfile")
On 2.2.20, this would crash with serialization errors. On 2.2.21, it works as expected.
For reference, without Parcelize you'd need to write something like 100 lines of boilerplate per class to implement Parcelable manually. The annotation saves a lot of time.
WebAssembly Fixes (If You're Doing KMP)
If you're building Kotlin Multiplatform apps targeting web, Safari 18.2 and 18.3 had issues with exception handling in WebAssembly code. Exceptions wouldn't propagate correctly, making error handling unreliable.
Example of shared code that was problematic:
// commonMain
class PaymentProcessor(private val logger: PlatformLogger) {
suspend fun processPayment(
amount: Double,
cardNumber: String
): PaymentResult {
return try {
validateCardNumber(cardNumber)
val transactionId = callPaymentAPI(amount, cardNumber)
PaymentResult.Success(transactionId, amount)
} catch (e: InvalidCardException) {
// Before 2.2.21: This catch block wouldn't work in Safari
logger.error("Invalid card: ${e.message}", e)
PaymentResult.Error(e.message ?: "Invalid card details")
} catch (e: Exception) {
logger.error("Unexpected error: ${e.message}", e)
PaymentResult.Error("An unexpected error occurred")
}
}
private fun validateCardNumber(cardNumber: String) {
if (cardNumber.length != 16 || !cardNumber.all { it.isDigit() }) {
throw InvalidCardException("Card number must be 16 digits")
}
}
}
// wasmJsMain
actual class PlatformLogger {
actual fun error(message: String, throwable: Throwable?) {
// Before 2.2.21: This would fail in Safari
// After 2.2.21: Works correctly
console.error(message, throwable)
}
}
Now exceptions propagate correctly across all browsers. If you're not doing KMP with Wasm targets, this doesn't affect you.
Gradle Configuration Cache Actually Works
This is the improvement I'm most excited about for day-to-day development. Multi-module projects with 5+ modules see significant build time improvements.
My setup:
// settings.gradle.kts
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
rootProject.name = "ShoppingApp"
include(":app")
include(":feature:auth")
include(":feature:catalog")
include(":feature:cart")
include(":feature:checkout")
include(":core:network")
include(":core:database")
include(":core:ui")
Before 2.2.21, enabling configuration cache would cause Kotlin-related errors. Now it works.
# Before (configuration cache broken)
./gradlew clean build
# BUILD SUCCESSFUL in 3m 25s
# After with configuration cache
./gradlew build --configuration-cache
# BUILD SUCCESSFUL in 1m 45s
# Subsequent builds
./gradlew build --configuration-cache
# BUILD SUCCESSFUL in 22s
That 3+ minute build down to 22 seconds for incremental changes makes a real difference when you're iterating quickly.
Better Compiler Error Messages
This one's subtle but appreciated. The K2 compiler now gives more specific error messages with suggestions.
Before (confusing):
Error: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type User?
After:
Error: Cannot access 'name' on nullable type 'User?'
Suggestions:
1. Add null check: if (user != null) { ... }
2. Use safe call: user?.name
3. Use Elvis operator: user?.name ?: "Unknown"
Small thing, but it helps when onboarding new developers or when you're tired and can't remember the exact syntax.
How to Upgrade
Version catalog approach (what I use):
# gradle/libs.versions.toml
[versions]
kotlin = "2.2.21"
agp = "8.3.0"
compose = "1.6.0"
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
}
Or direct configuration:
// build.gradle.kts
plugins {
kotlin("android") version "2.2.21"
kotlin("plugin.parcelize") version "2.2.21"
}
After upgrading:
./gradlew clean
./gradlew build --configuration-cache
./gradlew test
If you're using Parcelize, test your navigation flows. If you're doing KMP with Wasm, test in Safari specifically.
Should You Upgrade?
Upgrade now if:
- You're on 2.2.20 and using Parcelize (it's broken there)
- You're doing KMP with WebAssembly targets
- You have a multi-module project and want faster builds
Upgrade soon if:
- You have Compose apps with heavy list usage
- You want the better compiler error messages
Wait if:
- You're on an older stable version (2.1.x) and everything works
- You're mid-release and don't want to introduce variables
The upgrade from 2.2.0 to 2.2.21 was smooth for me. No breaking changes in my codebase, just the fixes and improvements described above.