# Full Circle: Production Kotlin Multiplatform for TechnoAlpin

[Jiri Parizek](https://www.strv.com/blog/authors/jiri) Android Engineer

---

TL;DR We started our production Kotlin Multiplatform project with a complex multi-framework setup inspired by KMMBridge. It collapsed at five modules. We pivoted to the JetBrains-recommended monorepo and shipped 31. Here's what we learned.

This is part one of a two-part series on the TechnoAlpin project. Part two covers the strategic exceptions where we shared UI in Compose Multiplatform.

At STRV, we like to stress-test new tech before we adopt it. When the project with TechnoAlpin, a leader in snowmaking systems for ski resorts worldwide, kicked off, we saw the perfect opportunity to do that with Kotlin Multiplatform (KMP). Our goal was to rewrite an existing Android codebase into KMP to launch an iOS application first.

TechnoAlpin's snow guns span many generations, from older units to the latest models, and the app has to talk to all of them. On top of that, we dealt with architecture conflicts, memory leaks and tooling quirks. This is the story of how we built it, broke it and why we eventually went back to basics.

### The Architecture Odyssey: Failing Fast

Our initial approach was ambitious. Inspired by advanced setups like KMMBridge, we designed a multi-repository system where every feature module was built as its own dedicated `.xcframework`.

### Where the Micro-Framework Approach Broke Down

We built a custom Gradle plugin to automate the workflow. It handled module builds, uploaded them to artifact hosting and generated a new Package.swift with updated versions. To test KMP changes on iOS, we used this plugin to switch our Package.swift from referencing remote artifacts to a locally generated `.xcframework`.

It felt like modern automation magic until we hit a wall.

With only five feature modules, build times spiraled. Because Kotlin/Native compiles to static libraries by default, linking multiple KMP frameworks into a single iOS app caused three concrete problems:
- **Bloat:** Core dependencies like the Kotlin Standard Library were duplicated inside every framework.
- **Linking conflicts:** The iOS linker struggled with duplicate symbols.
- **CI timeouts:** Building just those five frameworks grew our CI times exponentially.

### How We Restructured to Ship 31 KMP Modules

If we were struggling with five modules, we would never make it to 31 feature modules for launch. The constant context switching and build overhead made the development cycle feel heavy and cumbersome. We had to pivot.

We moved through several phases, including an "Umbrella" framework, to solve linking errors and workflow friction. That eventually led us to the **monorepo setup** we now treat as our standard for production KMP.

### Kotlin ↔ Swift Bridge

The Technoalpin Kotlin framework is linked into the iOS app directly: an Xcode Run Script Build Phase runs `./gradlew embedAndSignAppleFrameworkForXcode` (with `Moko's copyFrameworkResourcesToApp`), so the static `ATASSpro` framework is built and embedded on every Xcode build.

The Kotlin → Swift bridge layers three libraries: SKIE generates idiomatic Swift interop (sealed/enum mapping, `suspend` → `async`, SwiftUI observation); KMP-NativeCoroutines turns `StateFlows` into Combine publishers via `@NativeCoroutinesState` and KMP-ObservableViewModel provides the `UiStateViewModel` base class so Kotlin ViewModels behave as Swift observables.

On iOS, each feature uses a Store pattern: a SwiftUI `@Observable` Store holds the KMP ViewModel and binds its flow into a Combine pipeline. Because a KMP ViewModel typically outlives the SwiftUI view that displays it (the DI container or parent coordinator owns it), SwiftUI's view tree alone can't tell the ViewModel when it is actually on-screen, so the SDK defines a small `LifecycleViewModel` interface (`onViewAppeared()` / `onViewDisappeared()`), exposed to Swift via `@ObjCName`, that `UiStateViewModel` implements to toggle things like auto-refresh, polling and subscriptions on and off. The Store wires this up by forwarding SwiftUI's `.onAppear` / `.onDisappear` callbacks into those two methods, so background work runs only while the screen is visible; the ViewModel's own `viewModelScope` is then cleared separately by KMP-ObservableViewModel's `onCleared()` when the ViewModel itself is destroyed.

*(For a deep dive into the technical specifics of that final architecture, check out our post on [Kotlin Multiplatform in Production](https://www.strv.com/blog/kotlin-multiplatform-in-production-what-worked-what-didn-t).)*

### What a Production-Ready KMP Monorepo Looks Like

By the end of the project, we scrapped the complex custom tooling. We adopted the standard JetBrains recommendation: a single monorepo and the standard `embedAndSignAppleFrameworkForXcode` Gradle task.

That took us "full circle." We tried to outsmart the standard setup with complex automation, only to find the official recommendation was the simplest setup that actually held up at our scale. It let us focus on what mattered: delivering a high-quality iOS experience while building a foundation for the Android app to follow.

Up next in this series: the strategic exceptions where we shared UI in Compose Multiplatform.

---

Don't miss anything