How We Turned Kotlin/Native on iOS into a First-Class Development Experience
This article details a deep engineering effort that transforms Kotlin Multiplatform on iOS into a first‑class development environment, covering the current pain points, root causes, a redesigned Bazel‑based build pipeline, parallel compilation techniques, performance gains, and future roadmap for mobile developers.
Introduction
This article is the fourth in the KMP Technology and Practice series, focusing on making Kotlin/Native a first‑class citizen on iOS, matching or surpassing the Apple‑provided ObjC/Swift workflow.
Why iOS Development Feels Unpleasant
According to the KMP 2025 roadmap, the top priority is to improve the “joy of coding” on iOS because the current feedback loop “edit‑one‑line → see result” is too long.
We define a pleasant experience as the shortest possible inner loop from code change to visible result.
Three Basic Demands
Coding : low‑latency input, smart completion, immediate error reporting; seamless navigation between ObjC/Swift/C/Kotlin.
Build System : incremental first, remote cache, distributed compilation, reproducible builds.
Debugging : complete symbols, stable breakpoints, consistent paths; fast and reliable attach/hot‑restart.
Current State of the KMP Ecosystem
Coding : Xcode’s support for ObjC/Swift is limited; Kotlin‑Native in IDEA works well, but most developers still use Xcode, leading to a fragmented experience.
Build System : Kotlin Gradle Plugin (KGP) is Gradle‑based, but Gradle is rarely used in iOS, and KGP’s incremental compilation, stability and reproducibility are far from the “never clean build” goal.
Debugging : Xcode’s Instruments are excellent, but KGP relies on the third‑party xcode‑kotlin bridge, causing source‑map and symbol issues.
Root Causes
The ecosystem tries to adapt to iOS through many parallel, incompatible paths (CocoaPods, SPM, Tuist, CMake, native .xcodeproj). Lack of a unified, declarative, reproducible standard leads to divergent dependency resolution, caching, and debugging behaviours.
Our Goals
Coding : achieve the same read/write experience for Kotlin‑Native in Xcode as Swift/ObjC, and in IDEA/VSCode as native Kotlin.
Build System : replace the Xcode‑centric workflow with Bazel, enable high‑concurrency incremental builds, and improve Kotlin‑Native’s parallel compilation capability.
Debugging : provide stable debugging in Xcode and VSCode comparable to native Swift/ObjC.
Decomposing Kotlin‑Native Issues
We identified six critical issues (marked 😈) such as lack of module‑level interop, missing source‑map in .klib, absent private implementation_deps, redundant IR checks, low concurrency in IR→Object aggregation, and naming‑clash problems.
Our Optimizations
We redesigned the architecture: split the original .klib into ori.klib, abi.klib, final.klib, final_cinterop.klib, and generated Clang modules via Bazel’s headerInfo. We also cut implementation_deps propagation and compiled each Kotlin module to a static .a file for maximal parallelism.
Parallel Compilation
Each kt_library is compiled independently with konanc -p static_cache, producing a .a file. The final artifact compiler is reduced to a simple LLVM linker, and remote caching is enabled, achieving “never clean build”.
Performance Results
Clean iOS build time reduced from 1959 s to 1865 s (≈5 %).
Final artifact compilation dropped from 590.7 s to 12.6 s (≈98 % reduction).
Incremental builds on small changes go from minutes to seconds; large changes see up to 40× speed‑up.
Summary
By deeply engineering the Kotlin/Native pipeline with Bazel, we turned KMP on iOS into a first‑class development experience, delivering faster builds, true module‑level interop, reliable debugging, and a feedback loop comparable to native Swift/ObjC.
Future Outlook
Short‑term: unify IDE workflow (VSCode + Kotlin LSP, IDEA + sourcekit‑lsp) and adopt Swift Direct Export. Mid‑term: pursue hot‑reload and richer diagnostics. Long‑term: give back to the community and influence the upstream Kotlin/Native roadmap.
Recommendations
Prefer pure‑Kotlin solutions when possible to minimise cross‑language glue.
For large monorepos, a custom Bazel pipeline remains essential.
Share findings with JetBrains and the open‑source community.
KlibInfo = provider(
fields = {
"klibs": "The output klibs",
"transitive_compile_klibs": "The transitive compile klibs",
"transitive_compile_interface_klibs": "The transitive compile interface klibs",
"transitive_klibs": "The transitive klibs (All transitive klibs)",
"transitive_interface_klibs": "The dependencies for compile klibs (attr: deps + implementation_deps)"
}
)Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
