How did we discover the iOS linkage crash in our Kotlin Multiplatform project?
While working on a high-security cross-platform FinTech application, our team was tasked with unifying the Android and iOS codebases using Kotlin Multiplatform (KMP) and Compose Multiplatform. The goal was to share not just business logic, but also the entire UI layer and navigation routing logic to accelerate feature delivery. Everything worked flawlessly during local Android testing. The screens transitioned smoothly, state was preserved, and performance was excellent.
However, during a routine deployment to our iOS test devices, we realized we had a critical issue. The app launched, but the moment the initial routing graph was invoked, the application crashed instantaneously. There were no graceful degradation or caught exceptions—just a hard runtime native crash. This situation immediately halted our iOS testing pipeline. When companies hire software developer teams for cross-platform delivery, resolving deep platform-specific anomalies like this quickly is what separates robust engineering from endless debugging cycles. This challenge inspired the following technical breakdown so other teams can avoid the same dependency trap when scaling their multiplatform architectures.
Why does dependency alignment matter in Kotlin Multiplatform routing?
In a standard Android environment, Jetpack Navigation Compose is a mature, tightly coupled library that relies heavily on Android-specific lifecycle and saved state components. For our FinTech platform, the business use case required deep linking, complex authentication flows, and secure state preservation across multiple screens. To achieve this in KMP, we utilized the JetBrains port of Navigation Compose.
The problem arises in how Kotlin Native compiles and links these dependencies for the iOS target. Unlike the JVM, where classpath conflicts often surface as easily identifiable version clashes, Kotlin/Native compiles code into intermediate representation (IR). If the version of the Navigation library expects a specific function signature in the Lifecycle or SavedState library, and that signature has been altered or is missing in the ported version you are using, the compiler might still generate a binary that ultimately fails upon execution.
What caused the IrLinkageError runtime crash on iOS?
Upon inspecting the iOS crash logs via Xcode and our crash reporting tools, we found the following stack trace:
Uncaught Kotlin exception: kotlin.native.internal.IrLinkageError: Function 'performRestore' can not be called: No function found for symbol 'androidx.savedstate/SavedStateRegistryController.performRestore|performRestore(androidx.core.bundle.Bundle?){}[0]'This IrLinkageError is a classic symptom of binary incompatibility in Kotlin Multiplatform. The JetBrains fork of Navigation Compose we were using (version 2.8.0-alpha10) was trying to invoke the performRestore function on the SavedStateRegistryController. However, the specific version of the Lifecycle and SavedState libraries pulled into the dependency tree either lacked this function on the iOS target or had a completely different signature.
In our initial libs.versions.toml, we had aggressively updated certain libraries while lagging on others. We were mixing Kotlin 2.1.x with an older Compose Multiplatform version and mismatched AndroidX Lifecycle versions. The iOS binary was attempting to link an intermediate representation that fundamentally did not align with the provided native stubs.
How did we diagnose and approach the Compose Multiplatform version mismatch?
To diagnose the issue, we first generated a dependency tree for both our Android and iOS targets. We needed to map out exactly which transitive versions of androidx.lifecycle and androidx.savedstate were being resolved. We noticed that while Gradle was successfully resolving the dependencies for the JVM, the iOS source sets were resolving a fractured mix of JetBrains-ported artifacts and standard AndroidX stubs.
Our reasoning process focused on establishing a strict version matrix. Because JetBrains ports Jetpack libraries in synchronized waves, picking an arbitrary version of Navigation and pairing it with a different era of Compose Multiplatform almost always guarantees an IrLinkageError.
What alternative routing solutions did we consider?
Before strictly enforcing the version matrix, we evaluated a few architectural pivots. We considered these solutions as well:
- Reverting to native iOS routing: We explored decoupling the UI and using SwiftUI for iOS navigation while keeping Compose for Android. This was rejected as it defeated the core business value of a unified UI layer.
- Switching to third-party KMP routers: We evaluated libraries like Voyager and Decompose. While excellent tools, migrating our already complex Jetpack-based navigation graph would have severely delayed the release.
- Manually patching the SavedState registry: We considered writing expect/actual implementations to bypass the broken restore call. This was deemed too hacky and a maintenance nightmare for future updates.
What is the stable version combination for cross-platform navigation?
After systematic testing, we identified that the root fix required stepping back from cutting-edge alpha Kotlin compilers and aligning strictly with the JetBrains compatibility matrix. The solution involved harmonizing Kotlin, Compose Multiplatform, and the JetBrains forks of Lifecycle and Navigation.
Here is the proven, stable configuration we implemented in our version catalog:
[versions]
agp = "8.2.2"
kotlin = "2.0.20"
composeMultiplatform = "1.7.0-rc01"
androidx-lifecycle = "2.8.2"
navigationCompose = "2.8.0-alpha10"
[libraries]
jetbrains-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
jetbrains-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
Validation Steps:
- We ensured all navigation dependencies used the
org.jetbrains.androidxgroup ID rather than the standardandroidxgroup ID, which lacks the iOS native implementations. - We downgraded Kotlin from 2.1.x to the stable 2.0.20 release, ensuring the IR compiler matched the Compose compiler plugin natively.
- We performed a full clean build and invalidated caches:
./gradlew clean iosX64Binaries. - The iOS app successfully booted, and deep-link routing functioned identically to the Android implementation.
What actionable lessons can engineering teams apply for Kotlin Multiplatform?
When organizations decide to hire Kotlin developers for multiplatform architecture, it is crucial that the team understands the nuances of native compilation. Here are the actionable insights from this experience:
- Strictly Follow the Matrix: Never arbitrarily bump KMP libraries. Kotlin, Compose Multiplatform, and JetBrains AndroidX ports are tightly coupled. Always verify compatibility in the official JetBrains release notes.
- Beware of the Group IDs: Ensure you are using
org.jetbrains.androidxand not the standardandroidxfor cross-platform shared UI modules. Mixing them will cause linkage errors. - Test iOS Targets Early: Do not rely solely on JVM/Android testing. KMP logic should be built and executed on iOS simulators continuously in your CI/CD pipeline to catch
IrLinkageErrorcrashes at the PR level. - Understand IR Linkage: Realize that KMP dependency resolution on iOS is fundamentally different from standard Gradle JVM resolution. A successful Gradle sync does not guarantee a successful iOS native binary build.
- Manage Transitive State: Libraries like Navigation rely on SavedState and ViewModel. If you force a specific version of Navigation, use Gradle constraints to enforce the corresponding versions for its transitive dependencies.
How do these insights help you scale your cross-platform engineering?
Dependency management in Kotlin Multiplatform goes beyond simple version bumps; it requires a deep architectural understanding of how intermediate representations link across disparate operating systems. By diagnosing the IrLinkageError and establishing a strict version catalog, our team stabilized the FinTech application, allowing for rapid, unified feature rollouts across both platforms. When enterprise leaders look to hire mobile app developers for cross-platform deployment, they need assurance that the team can navigate these deep framework complexities without compromising delivery timelines. If you are facing similar architectural bottlenecks or want to ensure your next KMP project is built on a solid foundation, contact us.
Social Hashtags
#KotlinMultiplatform #ComposeMultiplatform #Kotlin #KMP #iOSDevelopment #AndroidDevelopment #JetpackCompose #ComposeUI #TechBlog #MobileDevelopment #CrossPlatform #SoftwareEngineering #AppDevelopment #KotlinNative #NavigationCompose #AndroidX #iOS #FinTech #DeveloperTips #Debugging
Frequently Asked Questions
Standard Jetpack Navigation Compose is built exclusively for Android. When using KMP, you must use the JetBrains ported version. Crashes on iOS usually occur due to version mismatches between the ported Navigation library and underlying ported lifecycle/saved-state libraries, leading to missing binary symbols during native compilation.
An IrLinkageError happens when the Kotlin compiler generates an Intermediate Representation (IR) that references a class, method, or property that does not exist in the linked binary at runtime. This is almost always caused by dependency version mismatches.
It is generally discouraged unless you are testing experimental features. Compose Multiplatform compiler plugins are tightly bound to specific Kotlin versions. Using an unsupported alpha version of Kotlin can lead to compilation failures or runtime linkage errors.
Always reference the official Compose Multiplatform release notes provided by JetBrains. They publish tested version matrices detailing which versions of org.jetbrains.androidx.navigation and org.jetbrains.androidx.lifecycle are compatible with specific Compose Multiplatform releases.
Yes. Many teams that hire app developer to create a mobile app utilizing KMP opt for community-driven routing solutions like Voyager or Decompose, which were built from the ground up for Kotlin Multiplatform and often bypass the complexities of ported AndroidX libraries.
Success Stories That Inspire
See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.

California-based SMB Hired Dedicated Developers to Build a Photography SaaS Platform

Swedish Agency Built a Laravel-Based Staffing System by Hiring a Dedicated Remote Team

















