A year ago, we started migrating a large Polish e-commerce app to Kotlin Multiplatform. Here's what actually happened.
The Setup
The app serves millions of users. Before KMP, we had two separate codebases - native Android (Kotlin) and native iOS (Swift). The usual story: duplicated business logic, bugs fixed on one platform but not the other, two teams working in parallel on identical problems.
KMP promised to fix this. Did it?
What We Shared (And What We Didn't)
We went with a layered architecture approach rather than feature-based modules. This turned out to be the right call.
Shared code (KMP):
- Domain layer (business logic, use cases)
- Repositories
- Networking
- Local database
Stayed native:
- ViewModels
- UI (Compose on Android, SwiftUI on iOS)
This split felt natural. The domain layer is where the real duplication pain was. UI differences between platforms are legitimate - users expect platform-native behavior.
The Firebase Problem
This one caught us off guard.
Firebase SDK is platform-specific. But our shared domain layer needed Crashlytics for error tracking and Analytics for business events. The solution: dependency injection using Kotlin's expect/actual mechanism.
// In shared code
expect class AnalyticsTracker {
fun logEvent(name: String, params: Map<String, Any>)
}
// Platform creates Firebase instance natively,
// then injects it into shared core
It works, but it's not pretty. Every Firebase feature you want in shared code requires this dance. If I were starting today, I'd think harder about what truly needs to be in the shared layer vs. what can stay platform-specific.
Build Times: The Good News
I expected build times to explode. They didn't.
The Kotlin compiler has gotten significantly better. Our incremental builds are reasonable. Full rebuilds take longer, sure, but not dramatically worse than before.
This was a pleasant surprise.
The Two-IDE Reality
Here's something nobody warns you about: your iOS developers will live in two IDEs.
They need Xcode for iOS-specific code, debugging, and running the app. But for shared Kotlin code, Android Studio (or Fleet) is significantly better. Syntax highlighting, autocomplete, refactoring - it all works better in JetBrains tools.
Some of our iOS devs resisted this at first. Eventually, everyone adapted. But factor this into your onboarding time.
CI/CD Gets Complicated
Our CI pipeline needed serious rework.
The shared KMP module is its own thing now. iOS and Android apps depend on it. We ended up using Git submodules to manage this - the shared code lives in a separate repo, and both platform repos reference it.
This adds complexity:
- PRs that touch shared code need coordinated merges
- Version pinning becomes important
- Your CI needs to understand these dependencies
Not insurmountable, but definitely more moving parts than two independent native apps.
The Biggest Regret
We should have written more unit tests for shared code from day one.
When your business logic lives in one place, a bug affects both platforms simultaneously. That's the flip side of "fix once, fixed everywhere." Before KMP, a logic bug might only hit Android users. Now it hits everyone.
Shared code deserves higher test coverage than platform-specific code. We learned this the hard way.
Would I Choose KMP Again?
Yes.
The promise is real: write your domain logic once, use it everywhere. After a year, we have genuinely less code duplication and fewer "fixed on Android but not iOS" bugs.
But go in with realistic expectations:
- Architecture matters more than ever. Layer-based modularization worked better for us than feature-based.
- Platform boundaries are real. Don't try to share UI or fight platform-specific SDKs.
- Testing discipline is non-negotiable.
- Your iOS team needs to get comfortable with Kotlin and JetBrains tooling.
KMP is production-ready. It's not magic. It's a tool that works if you respect its constraints.
I'm a mobile developer specializing in Android, iOS, and Kotlin Multiplatform. Currently helping startups build cross-platform apps at MobileDev Node.
Top comments (0)