Pavan Rangani

HomeBlogKotlin Multiplatform Mobile Development: Guide 2026

Kotlin Multiplatform Mobile Development: Guide 2026

By Pavan Rangani · March 6, 2026 · Mobile Development

Kotlin Multiplatform Mobile Development: Guide 2026

Kotlin Multiplatform Mobile: Shared Logic, Native UI

Kotlin multiplatform mobile enables sharing business logic, networking, and data layers between Android and iOS while keeping native UI on each platform. Therefore, teams maintain the native look and feel users expect while eliminating duplicate business logic implementation. As a result, development velocity increases without sacrificing platform-specific user experiences. Crucially, KMP is a code-sharing strategy rather than a UI framework, which is exactly why it sidesteps the “uncanny valley” problems that plague abstraction-heavy cross-platform toolkits.

KMP Architecture Overview

The shared module contains platform-agnostic code including data models, repository patterns, and business rules. Moreover, expect/actual declarations provide a mechanism for platform-specific implementations within the shared codebase. Consequently, meaningful code-sharing ratios are achievable without compromising native capabilities — though the exact percentage depends heavily on how much of your app is logic versus UI. Compose Multiplatform extends the sharing opportunity to the UI layer for teams comfortable with declarative UI across platforms. Furthermore, the shared module can integrate with Swift Package Manager on iOS for cleaner dependency management.

Kotlin multiplatform mobile app development
KMP shares business logic while maintaining native UI

The expect/actual Mechanism in Practice

The expect/actual pattern is how KMP reaches into platform APIs without leaking them into shared code. You declare an expected contract in commonMain and supply a concrete implementation in each target source set. A typical example is platform-specific storage or device identity.

// commonMain: the contract every platform must satisfy
expect class PlatformContext

expect fun createSettings(context: PlatformContext): Settings

expect val platformName: String

// androidMain: backed by SharedPreferences
actual typealias PlatformContext = android.content.Context

actual fun createSettings(context: PlatformContext): Settings {
    val prefs = context.getSharedPreferences("app", Context.MODE_PRIVATE)
    return SharedPreferencesSettings(prefs)
}

actual val platformName: String = "Android ${'
		
		
	

}{Build.VERSION.SDK_INT}"

// iosMain: backed by NSUserDefaults
actual class PlatformContext

actual fun createSettings(context: PlatformContext): Settings =
    NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults)

actual val platformName: String =
    "iOS ${'
		
		
	

}{UIDevice.currentDevice.systemVersion}"

The discipline this enforces is valuable: shared code calls createSettings() and never imports SharedPreferences or NSUserDefaults directly. As a result, the dependency on platform APIs is contained to a thin boundary, and the bulk of your logic stays testable on the JVM. Use it sparingly, though — over-applying expect/actual for things a library already abstracts just adds boilerplate.

Kotlin Multiplatform Mobile Data Layer

SQLDelight provides cross-platform database access with type-safe SQL queries that compile to native code on each platform. Additionally, the Ktor client handles networking with platform-specific HTTP engines for optimal performance. For example, OkHttp on Android and the Darwin engine over URLSession on iOS provide an idiomatic networking experience on each platform.

// Shared data layer with SQLDelight + Ktor
class ProductRepository(
    private val database: AppDatabase,
    private val api: ProductApi
) {
    suspend fun getProducts(): Flow<List<Product>> {
        // Fetch from API and cache in local database
        try {
            val remote = api.fetchProducts()
            database.productQueries.transaction {
                remote.forEach { product ->
                    database.productQueries.insertOrReplace(
                        id = product.id,
                        name = product.name,
                        price = product.price,
                        category = product.category,
                        updatedAt = Clock.System.now().toEpochMilliseconds()
                    )
                }
            }
        } catch (e: Exception) {
            println("Network error, using cached data: " + e.message)
        }
        // Return reactive flow from local database
        return database.productQueries.selectAll()
            .asFlow()
            .mapToList(Dispatchers.Default)
    }
}

The repository pattern abstracts data sources so consuming code never deals with API calls or database queries directly. Therefore, the same business logic works identically on both platforms. Notice the offline-first ordering: the flow always emits from the local database, while the network call merely refreshes the cache. Consequently, the UI shows cached data instantly and updates when fresh data arrives, which is the behavior users associate with well-built native apps.

Serialization is the other piece that makes this layer portable. The kotlinx.serialization library plugs directly into Ktor’s content negotiation, so a single @Serializable data class describes the wire format once and decodes identically on Android and iOS. Threading deserves attention too: coroutines in shared code dispatch onto platform-appropriate executors, but on Kotlin/Native you should be deliberate about which dispatcher does the work, because the old strict memory model punished naive cross-thread sharing. The newer memory manager relaxes most of those constraints, yet treating shared state as immutable and passing data through flows remains the safest pattern. Build configuration ties it together — the shared module declares its dependencies in commonMain and adds engine-specific artifacts in androidMain and iosMain, which keeps each platform pulling only the networking and database drivers it actually needs.

iOS Integration Best Practices

Integrating KMP with SwiftUI requires care around Kotlin/Native memory management and coroutine bridging. However, libraries like SKIE generate Swift-friendly APIs — mapping Kotlin Flow to Swift AsyncSequence and sealed classes to Swift enums — so the shared code feels natural in Swift. In contrast to React Native or Flutter, KMP does not replace the native UI layer, which avoids platform abstraction issues but means iOS engineers still write SwiftUI by hand.

Mobile cross-platform development
iOS integration preserves native SwiftUI experiences

Two practical points smooth the iOS path. First, the shared module ships as an XCFramework; wiring it through CocoaPods or Swift Package Manager keeps Xcode builds reproducible. Second, suspend functions cross the bridge as completion handlers unless a wrapper like SKIE adapts them, so decide early whether your iOS team consumes raw Kotlin/Native APIs or a generated Swift surface. Getting that decision right up front saves a great deal of friction later.

Testing Shared Code

Shared module tests run on both the JVM and native targets to verify platform-independent behavior. Additionally, expect/actual test utilities handle platform-specific setup like database drivers and HTTP mock servers. Specifically, the commonTest source set ensures business logic tests execute across all target platforms, so a passing test on the JVM is reinforced by the same test running natively. Ktor’s MockEngine and SQLDelight’s in-memory driver make these tests fast and deterministic without touching a real network or disk. Because the same suite runs on every target, you catch the rare cases where a shared dependency behaves differently on Kotlin/Native than on the JVM before that difference ever reaches a device, which is precisely the kind of regression that is painful to debug in production.

Mobile app testing and quality
Cross-platform testing ensures consistent behavior everywhere

When NOT to Reach for KMP

Being candid about the trade-offs builds trust in the recommendation. KMP shines when an app is logic-heavy — sync engines, financial calculations, complex domain rules — and when you already have Kotlin expertise on the team. It is a weaker fit when the app is mostly UI with thin logic, because then you are sharing very little while still paying the build-tooling and Kotlin/Native learning costs. The iOS toolchain is also less mature than Android’s: native debugging can be rougher, and some libraries lag on Apple-silicon or new Xcode versions. Therefore, prototype the iOS build early, confirm your critical dependencies are supported, and only then commit. For a contrasting web-first option, our guide on progressive web app development covers when a single web codebase makes more sense than shared native logic.

Related Reading:

Further Resources:

In conclusion, Kotlin multiplatform mobile development offers the best balance between code sharing and native platform fidelity. Therefore, adopt KMP to eliminate duplicated business logic while preserving the native user experience your users expect — provided your app is logic-rich and your team is ready for the iOS toolchain.

← Back to all articles