DEV Community

rana bilal
rana bilal

Posted on

#KotlinMultiplatform #ComposeMultiplatform #AndroidDev #MobileArchitecture

Two different bets

Flutter’s bet is total UI ownership. One rendering engine (Impeller), one widget tree, pixel-identical output on iOS and Android. You get to visual parity fastest, at the cost of leaving your native UI investment behind — native accessibility semantics, platform-specific animations, and deep OS integrations all have to be re-built or bridged.

Kotlin Multiplatform’s bet is shared logic, native UI. Your networking, caching, validation, and view-model layer live in commonMain; each platform still renders its own UI — natively, or via Compose Multiplatform if you also want to share UI code. You keep native performance characteristics and existing UI investment, at the cost of a slower path to full UI parity if that’s actually your goal.

The framework

Before touching a single shared module, I run a team through four checks:

  1. Audit your last two quarters of production bugs. Bucket them: UI bugs (layout, rendering, platform-specific visual glitches) vs. logic bugs (state management, race conditions, data inconsistency between platforms). If logic bugs dominate, that’s where KMP earns its keep fastest — you fix the bug once, in commonMain, instead of twice.

  2. Audit your native codebase’s actual quality, not its age. A five-year-old native UI layer that’s clean, well-tested, and fast is sunk cost you should protect. One that’s already accreted years of workarounds is a much weaker argument against a rewrite.

  3. Map your team’s skill distribution honestly. A full UI rewrite needs buy-in from engineers who didn’t choose it. If half your team is strong in Kotlin and weak in Dart, that’s not a blocker — but it is a cost you should schedule for, not discover.

  4. Prototype your riskiest shared module first. Not your easiest win — your riskiest one. If your team’s hardest, most stateful, most platform-divergent piece of logic (often offline sync, or anything touching background work) can’t share cleanly, you want to know in week two, not month six.

What a shared module boundary looks like

A minimal expect/actual boundary for something like secure storage:

// commonMain
expect class SecureStore() {
    fun putString(key: String, value: String)
    fun getString(key: String): String?
}

// androidMain
actual class SecureStore actual constructor() {
    private val prefs = EncryptedSharedPreferences.create(/* ... */)
    actual fun putString(key: String, value: String) = prefs.edit { putString(key, value) }
    actual fun getString(key: String): String? = prefs.getString(key, null)
}

// iosMain
actual class SecureStore actual constructor() {
    actual fun putString(key: String, value: String) { /* Keychain wrapper */ }
    actual fun getString(key: String): String? { /* Keychain wrapper */ }
}

Enter fullscreen mode Exit fullscreen mode

The value isn’t the boilerplate — it’s that your repository layer above this boundary, your view models, your business rules, are now written once and tested once.

What actually breaks this migration

Two failure modes I’ve watched teams hit: starting with the core (auth, networking) before the team has calibrated on anything smaller, and treating the migration as a big-bang rewrite instead of an incremental, always-shippable path. Both are avoidable by running step 4 honestly, before committing to a timeline anyone will hold you to.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.