Blog

What I learned shipping a running app with Kotlin Multiplatform, Compose, and a native Apple Watch app

I build Flux Run, an AI running coach for iPhone and Android, as a solo developer. The app itself is written once in Kotlin Multiplatform: run recording, pace and split maths, reminders, and the Compose Multiplatform UI. The AI coaching is not in the app: it runs on a separate backend service, and the app talks to it and shows what it returns. Around that sits a small amount of native code where the platform really matters. Here is where the line fell, and the two things that bit me.

What is shared, what is native

The rule I settled on: if it needs a system extension (watch, widget, Live Activity, foreground service), it is native; if it is a screen or a rule, it is shared.

Gotcha 1: an interstitial ad detached the Compose view and crashed the app

Apple rejected a build under guideline 2.1(a) for a crash after an ad played. Symbolicating the crash showed an unhandled Kotlin exception from Compose's hosting view controller: "Cannot attach layer if the view is not in the window hierarchy." The cause: a Compose AlertDialog opened while the AdMob interstitial was presented full-screen over the root view controller, which temporarily removes the Compose view from the window hierarchy. The fix was twofold: present the interstitial from its own temporary UIWindow so the Compose view stays attached, and only show that dialog within 30 minutes of a purchase started in the paywall. If you mix Compose Multiplatform on iOS with full-screen native presentations, check what your Compose view's window state is when they overlap.

Gotcha 2: the iOS app can silently ship old Kotlin

The iOS app consumes the shared module as an xcframework. If you build the app in Xcode without rebuilding the framework first, you get the previous Kotlin code with no error. I now rebuild the xcframework as its own explicit step before every iOS build, and I check the built IPA rather than trusting the build log.

Smaller lessons

Why KMP for this app

One team of one. Sharing the run logic and UI across two phones, and keeping the watch apps native, let me change a screen or a rule once and see it on iPhone and Android the same day. The AI side lives on the server, so it can change without shipping a new app.

If you want to try it: Flux Run is free on iPhone and Android.