Kotlin Multiplatform creates a tempting question: how much can we share? A better question is: which knowledge should have one owner, and which behaviour needs to remain native?

For a product delivered through Compose on Android and SwiftUI on iOS, my preferred boundary is straightforward. Share domain contracts, repositories and use cases. Keep navigation, screen state adaptation and interface composition on each platform.

Share product truth

Business rules should not quietly diverge between two clients. Query contracts, validation, domain models, data mapping and use cases are strong candidates for shared Kotlin because they describe the product rather than a rendering framework.

The shared layer should stay free of platform UI concepts. Common Kotlin cannot reference Swift declarations, but a shared contract can still be distorted around SwiftUI navigation or Android presentation concerns. If a domain model knows about a Compose state holder or encodes a platform screen flow, the boundary is already leaking. Platform-agnostic contracts make the shared code easier to test and less expensive to evolve.

Keep presentation accountable to the platform

Android and iOS have different navigation systems, lifecycle behaviour, accessibility conventions and user expectations. Hiding those differences behind a common screen abstraction often relocates complexity rather than removing it.

Platform-owned presentation lets Android use its ViewModel and StateFlow conventions while iOS adapts shared work into observable Swift state. Compose and SwiftUI remain free to express their native layout, navigation and interaction patterns.

This is not duplicated architecture. It is a deliberate last-mile adaptation around one shared product core.

Shared multiplatform ViewModels are also a valid choice. AndroidX lifecycle APIs and other KMP libraries can reduce presentation duplication when the screens, lifecycle needs and state transitions genuinely align. I keep presentation native when platform interaction patterns or team ownership differ enough that a shared ViewModel would become a negotiation layer. The boundary is a product decision, not a rule that shared presentation is inherently wrong.

Model the bridge explicitly

Cross-language boundaries deserve the same care as an external API. Nullability, time types, enums, errors and asynchronous streams must be predictable from Swift as well as Kotlin.

Small public interfaces help. A focused use case is easier to expose and consume than a large service with platform-specific assumptions. Errors should be meaningful enough for each client to turn them into the right recovery experience. Time and money types should cross the bridge without implicit locale or precision changes.

Do not share the wrong operational concern

Android may schedule resilient refresh through WorkManager and combine it with an OkHttp or database cache. iOS may use background tasks and URLSession caching under different lifecycle limits. A shared business domain does not imply that both clients need the same persistence or refresh mechanism.

The same applies to analytics, deep links and release configuration. Share the semantic event or product rule where useful; keep the platform mechanism close to the platform that owns it.

A useful test

When deciding whether something belongs in shared Kotlin, ask three questions:

If the answer is yes to all three, it probably belongs in the shared layer. If not, keeping it native is not a failure of reuse—it may produce the clearest architecture.