Why I Chose Compose Multiplatform for FretPractice

I’m on record being loud about KMP, so you’d expect me to reach for Compose Multiplatform without a second thought. I didn’t. My honest default for shared UI is no, and I’ll explain why I overrode it for FretPractice, the guitar practice app I’m building at Mobrio Studio across 8 platforms.

If you’re weighing the same decision, the useful part of this post isn’t the conclusion. It’s the cost I was willing to pay to get there.


Where I actually stand

Kotlin. Long-time advocate, wrote books on it 8 years ago, more than half my posts touch it in some way. No argument to make here, it’s love.

Kotlin Multiplatform. Same. For sharing business logic across platforms, nothing else compiles one language to native binaries across targets the way KMP does. You can approximate it with C++ or Rust over FFI, but that complexity only pays off when you genuinely need low-level control. For business logic, KMP is the answer, full stop.

The one caveat I won’t soften: KMP has a steep curve for production apps. expect/actual, Gradle target management, iOS interop edge cases, CocoaPods wiring (SPM is still not production ready), none of it is an afternoon’s work. At JioCinema, getting the first engineers shipping confidently took weeks of pairing. If you go this route, have someone on the team who’s done it before.

Compose Multiplatform. Here’s where I split from the hype.

I love Compose on Android, once the declarative model clicks, it’s genuinely better. But my instinct for cross-platform UI has always been native: fewer platform conditionals polluting UI logic, a more correct feel per platform, better performance.

The reason is architectural, not aesthetic. KMP compiles to native binaries, LLVM on iOS/macOS, JVM bytecode on Android, JS/Wasm on web. Compose Multiplatform can’t do that for UI; it owns the rendering pipeline itself. On iOS, SwiftUI talks to Core Animation and Metal directly. Compose Multiplatform draws through Skiko, which then delegates to Metal. That extra layer isn’t free.

In production I’ve watched it surface as:

A caveat on these, because it matters: they’re patterns I’ve seen across production KMP/Compose work, not numbers I’ve measured on FretPractice yet. I haven’t benchmarked my own app rigorously, that’s coming, and it’s the subject of the follow-up post. So treat this section as informed expectation, not data.

And here’s what the skeptic in me has to concede: for the vast majority of apps, this overhead is negligible, small enough that a user never notices, and small enough that it’s a tuning problem, not a wall. Most of it responds to ordinary optimisation: stabilising recomposition, keeping list items cheap with stable keys, warming the rendering engine off the critical path, dropping to the platform where it’s genuinely cheaper. CMP has also closed a lot of this gap release over release, and keeps closing it. So the honest framing isn’t “CMP is slow”, it’s “CMP starts you a step behind native on a handful of axes, and how far behind you stay is mostly a function of how much you invest.” For FretPractice, the axes that actually bite live in the audio path, not the UI, which is exactly why I could accept the UI cost.

None of these are showstoppers for most apps. But they’re real enough that I planned around them.

The tradeoffs compound with each target. On Web (WebAssembly) you give up per-screen URLs and SEO, there are workarounds, but they’re workarounds. On Desktop you’re shipping a JVM app, not a native binary, which hits distribution size, startup, and OS integration.

Flutter. Contrary to what people assume, I like it. It has the same downsides of Compose Multiplatform, but it’s much faster to build in, the learning-curve is short regardless of background, and for UI-heavy apps with little native code the Skia/Impeller pipeline is mature and battle-tested. Even the ecosystem (libraries and Open Source SDKs) is much more mature than CMP. For a standard CRUD app (don’t underestimate the word CRUD, even a simple ecommerce app can be considered CRUD) with no special constraints, I’d pick Flutter over CMP, Dart notwithstanding.

So my prior going in was clear: shared UI is a compromise, and I don’t reach for it by default.


The decision that was actually hard

The easy part was eliminating Flutter. FretPractice isn’t a CRUD app. It needs:

Bridging native audio and BLE through Flutter’s platform-channel model at low latency is a non-starter. I’ve watched teams try; it doesn’t end well. Flutter was out in a paragraph.

That left the real fork, and it’s the one I want to spend the rest of this post on, because it’s where I had to argue myself out of my own default:

Compose Multiplatform vs. fully native UI per platform.

Here’s the case against CMP, made as honestly as I can: I’ve just spent half this post cataloguing its rendering costs. iOS is strategically important. I have strong native instincts and the skill to act on them. By my own framework (see below), a project where iOS matters and polish matters points at native UI. If I were advising someone else with my exact background, I might tell them to bite the bullet on per-platform UI.

So why didn’t I?

Because the unit of work is one person, and native UI for 8 platforms isn’t a polish tax, it’s a different project. Android, iOS, JVM Desktop, Web, watchOS, Wear OS, Android TV, Apple TV. The shared KMP business logic is identical either way; the only variable is how much UI I duplicate. Native means writing and maintaining eight UI layers solo. That doesn’t ship. It doesn’t even reach a testable MVP this year.

But “I’m solo so I cut corners” would be a weak argument on its own. The reason I’m comfortable with the corner is structural:

CMP keeps KMP’s actual strength, native code sits exactly where it belongs, and the shared layer doesn’t fight you. The DSP engine is pure Kotlin with JNI/cinterop at the edges. DRM and audio processing run natively. Compose is a layer above that, not woven through it. The seams are clean.

And that clean seam is what makes the corner safe to cut: if you structure the project well, swapping CMP UI for native UI later, shared KMP logic untouched, is a migration, not a rewrite. The hard, valuable, slow-to-build part (the audio engine, the hardware layer, the domain logic) is the part native UI would never have changed anyway. The UI is a thin skin over the real complexity. So choosing CMP today doesn’t foreclose native UI tomorrow; it defers it to when I can afford it. That optionality is the whole argument.

The KMP learning-curve caveat from earlier? It’s the one place my background pays a real dividend, I built production KMP infrastructure at scale, so the tax everyone else pays up front, I don’t. That’s not bragging; it’s the specific reason the math works for me and might not for you.


The framework, so you can disagree with me

The right choice doesn’t exist in a vacuum. Here’s the matrix I actually used, and the point of sharing it is that it should send you somewhere different if your constraints differ:

ScenarioRecommendation
CRUD app, solo dev, ship fastFlutter
Heavy native APIs (audio, BLE, hardware)KMP + CMP or KMP + native UI
Team of 6+, room for polish, iOS strategicKMP + native UI
Solo dev (or Small Team), native APIs, 3+ platformsKMP + CMP, FretPractice

Read across the rows and you’ll see CMP wins exactly one of them, and it happens to be mine. I didn’t choose Compose Multiplatform because it’s technically superior on most axes, I’ve spent this whole post arguing it isn’t. I chose it because it’s the best tradeoff for a one-person team that needs deep native capability across eight platforms and a path to native UI later if the rendering costs ever stop being acceptable.

If that’s not your situation, the matrix should point you elsewhere. That’s the post doing its job.


Next up: the DSP architecture, and the CMP performance issues I’ve actually hit on FretPractice with the numbers attached. That’s the post I owe you after this one.

rivu.dev

© 2026 Rivu Chakraborty

X / Twitter GitHub LinkedIn YouTube