Shared vs Platform-Specific Code
KMP provides several valid sharing boundaries. They are options, not a maturity ladder; a hybrid product is not a failed attempt at full sharing.
| Strategy | Shared | Platform-specific | Typical use |
|---|---|---|---|
| Shared core | Models, formatting, algorithms | Most application code | Libraries, experiments, first KMP adoption |
| Shared data | API, serialization, database logic, repositories | Presentation and UI | Incremental adoption |
| Shared data + domain | Data, repositories, business rules, use cases | ViewModels and UI | Independent native presentation teams |
| Shared presentation | Data, domain, state holders, UI state | Native UI and navigation | Consistent behavior with native UI |
| Shared UI | Most application code and Compose UI | Entry points and integrations | Small cross-platform teams and aligned products |
| Hybrid | Selected features, layers, or UI components | Everything else | Mature products with different platform needs |
Boundary by responsibility
Data sources and repositories
HTTP behavior, serialization, DTOs, SQL queries, cache policy, pagination, retry, and synchronization are often good shared candidates. Database drivers or factories, filesystem paths, secure storage, connectivity observation, background work, and push notifications commonly need platform adapters. The exact boundary depends on library target support and product requirements.
Repositories are especially valuable to share when they define a single source of truth, coordinate remote and local data, apply offline-first and freshness policies, and map DTO, database, and domain models. This keeps business behavior consistent rather than merely reusing transport code.
Domain and presentation
Domain models, validation, filtering, sorting, and real business rules are usually portable. Use cases should represent meaningful operations or orchestration, not wrap every repository method to imitate a layer diagram.
Presentation can share immutable UiState, actions, state holders or ViewModels, and loading, error, offline, retry, and recovery behavior exposed through StateFlow. Difficult boundaries remain: lifecycle ownership, saved state, navigation, permissions, SwiftUI and Compose conventions, and Kotlin/Swift interoperability.
UI may remain Jetpack Compose plus SwiftUI, use shared Compose Multiplatform screens, share only selected components, or share Android/Desktop Compose UI while iOS remains SwiftUI.
Interfaces and expect/actual
Prefer interfaces and injected implementations for behavior and replaceable dependencies such as storage, connectivity, clocks, analytics, external links, and other platform services:
interface ExternalLinks {
fun open(url: String)
}
class HelpPresenter(private val links: ExternalLinks) {
fun openHelp() = links.open("https://example.com/help")
}
Use expect/actual as a narrow bridge when an API has the same conceptual shape but needs target-specific implementation:
// commonMain
expect fun platformName(): String
// androidMain
actual fun platformName(): String = "Android"
Expected and actual classes are currently Beta; functions and properties can still be useful narrow bridges. Interfaces remain easier to fake, replace, or provide multiple times on one platform.
Practical heuristic: prefer interfaces for behavior and dependencies. Use expect/actual as a narrow bridge to platform APIs rather than as a replacement for architectural abstractions.
See Project Structure and Source Sets for placement and Architecture and UI Strategies for application-level choices.