Kotlin Multiplatform (KMP) has matured into a practical way to share non-UI code across Android, iOS, desktop, and web. It works best when you keep the shared layer focused and infrastructure disciplined. Below is a concise, production-ready checklist.
1) Scope the shared layer
- Share: domain models, validation, business rules, analytics wrappers, network + caching, feature flags, offline sync, and serialization.
- Don’t share: platform-specific UI, complex navigation, or OS-level permissions flows. Let Android Compose and SwiftUI own presentation.
2) Project structure that scales
- Modules:
:core:model,:core:util,:data:network,:data:cache,:feature:xyz-shared. Keep modules small and independently testable. - Expect/actual boundaries: Isolate platform code (e.g., keychain/keystore, file paths, reachability) behind
expectAPIs in shared, withactualimplementations per target.
3) Networking & serialization
- HTTP: Ktor client with per-platform engines (CIO/OkHttp on Android, Darwin on iOS). Configure timeouts and backoff centrally.
- Serialization: Kotlinx Serialization with stable, explicit schemas. Version payloads; add default values for forward compatibility.
- Error model: Normalize network/domain errors in shared so UI layers don’t guess.
4) Concurrency & lifecycle
- Coroutines: Use
Dispatchers.Default/IOin shared; exposesuspendAPIs. On iOS, export entry points that bridge to Swiftasync/await(via@Throwsandsuspend->asyncadapters). - Flow → iOS: Convert
FlowtoAsyncSequencewith a thin wrapper; avoid leaking coroutine scopes beyond the call site. - Cancellation: Propagate from UI; ensure all long-running work checks
isActive.
5) Caching & offline
- Storage: SQLDelight for typed SQL across platforms; prefer normalized tables and indices. For simple KV, use
multiplatform-settings. - Outbox: Implement an outbox table and idempotent sync functions in shared; platform layers only trigger and observe.
6) DI, logging, and analytics
- DI: Keep DI minimal (constructor injection + simple service locators). Heavy DI frameworks add friction on iOS.
- Logging: Provide a shared logger interface; map to OS logging (Logcat, os_log) in
actualcode. - Analytics: Define events in shared with typed parameters; route to Firebase/GA4 on Android and to your iOS provider via
actualadapters.
7) Testing strategy
- Unit (shared): Run JVM tests fast; include concurrency and serialization edge cases.
- Native tests: Add a small iOS test target to catch Kotlin/Native quirks (struct sizes, freezing).
- Contract tests: Snapshot JSON schemas and SQL migrations to prevent drift.
- Determinism: Fake time, random, and network in shared via
expectclocks and RNGs.
8) iOS integration realities
- Binary output: Use XCFrameworks with Gradle’s CocoaPods or direct SPM integration. Automate versioning and symbol stripping.
- Swift ergonomics: Provide Swift-friendly names (avoid deep package hierarchies),
sealed→ Swift enums via@Serializable+ discriminators where reasonable. - Debugging: Ship a symbols map; document how to step from Swift into shared code.
9) Performance & size
- Startup: Lazy-init heavy singletons; avoid large static initializers in shared.
- Binary size: Split modules; enable dead-code elimination. Measure IPA growth per release.
- Allocations: Watch for boxing and copy churn in tight loops; prefer immutable data but batch transformations.
10) Governance & release
- SemVer for shared: Treat shared as a library with changelogs. Breaking changes require deprecation windows.
- CI/CD: Build KMP on Linux; produce Android AARs and iOS XCFrameworks as artifacts. Run unit tests + lint on every PR.
- Observability: Emit identical event names/fields from both platforms; correlate via release/version tags.
Bottom line: KMP shines when it shares the essence—domain logic, data, and sync—while leaving each platform’s UI native. Keep module boundaries crisp, test the shared core thoroughly, and automate the iOS packaging pipeline. You’ll ship faster without compromising platform polish.




