DEV Community

M TECHUB LLc
M TECHUB LLc

Posted on

Cross Development: Sharing Business Logic With Kotlin Multiplatform

Most teams shipping on both Android and iOS eventually notice the same thing: the screens look different, but the rules behind them are identical. How a disc
 ount is calculated, what makes a password valid, when a cached token counts as expired. Those rules get written twice, in two languages, by two sets of developers. Then they drift apart.

Kotlin Multiplatform (KMP) targets that specific problem. This article explains what it is, what "sharing business logic" means in practice, and where it stops being the right tool.

Cross-platform vs. Kotlin Multiplatform

Cross-platform development is the broad idea of targeting several platforms without building everything separately for each one. Flutter, React Native, and KMP all sit under that umbrella, but they make very different trade-offs.

Kotlin Multiplatform is Kotlin's mechanism for compiling shared code to multiple targets. You choose which parts of your app are shared. That could be a validation module, a networking layer, or almost the whole app. Everything else stays platform-specific.

You may still see the older name "Kotlin Multiplatform Mobile" (KMM). It has been folded into the general KMP branding.

What "sharing business logic" means

Business logic is the code that decides what your app does, independent of how it looks. Typical examples:

Domain rules (pricing, eligibility, state machines)
Input validation
Data models and mapping between them
API communication and serialization
Caching and repository logic

What it usually does not include:

Screens, navigation, and animations (unless you opt into a shared UI framework)
Camera, biometrics, push notifications, and other device APIs
Platform-specific integrations like widgets or system extensions

The result is usually one Kotlin module that both apps depend on. The Android app uses it as a regular library. The iOS app consumes it as a framework, and Swift code calls into it.

A simple example

Here is a piece of logic you would otherwise write twice. It lives in the commonMain source set:

kotlin
// commonMain
enum class PasswordIssue { TOO_SHORT, MISSING_DIGIT, MISSING_UPPERCASE }

class PasswordPolicy(private val minLength: Int = 12) {

fun validate(password: String): List<PasswordIssue> = buildList {
    if (password.length < minLength) add(PasswordIssue.TOO_SHORT)
    if (password.none { it.isDigit() }) add(PasswordIssue.MISSING_DIGIT)
    if (password.none { it.isUpperCase() }) add(PasswordIssue.MISSING_UPPERCASE)
}
Enter fullscreen mode Exit fullscreen mode

}

There is nothing Android- or iOS-specific here, so both apps get identical behavior. The UI on each side decides how to present the errors: a Material text field on Android, a SwiftUI form on iOS. The rule lives in one place.

When shared code needs the platform: expect/actual

Sometimes common code needs something only the platform can provide. expect/actual declares a requirement in shared code and lets each platform fulfil it:

kotlin
// commonMain
expect fun currentPlatformName(): String
kotlin
// androidMain
actual fun currentPlatformName(): String =
"Android ${android.os.Build.VERSION.SDK_INT}"
kotlin
// iosMain
import platform.UIKit.UIDevice

actual fun currentPlatformName(): String =
UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion

This works well for small, focused pieces. For larger dependencies, many teams define an interface in common code and inject platform implementations instead. That keeps shared code easier to test, and it is where dependency injection fits naturally, whether through a KMP-friendly library like Koin or plain constructor injection.

What you can realistically share

Business rules and domain models. This is the most natural fit. Pure Kotlin with no platform dependencies is the easiest code to share.

Networking. Ktor Client is a common choice. It uses a platform-appropriate engine underneath (for example OkHttp on Android and Darwin on iOS), while your API code stays in common.

Serialization. kotlinx.serialization works across targets:

kotlin
@Serializable
data class Product(val id: String, val name: String, val priceCents: Long)

class ProductRepository(private val client: HttpClient) {
suspend fun fetchProducts(): List =
client.get("https://api.example.com/products").body()
}

(Imports omitted for brevity; you'd also install the content negotiation plugin on the client.)

Persistence. Libraries such as SQLDelight generate typed Kotlin APIs from SQL and support multiple platforms. Whether to share persistence depends on your needs. Some teams share it, others keep it native.

Concurrency. Kotlin coroutines and Flow are available in common code, so async logic like repositories and use cases can be shared.

What usually stays native

UI. Most KMP projects keep UI native: Jetpack Compose or Views on Android, SwiftUI or UIKit on iOS. This is a big part of the appeal for teams that want platform-native look and feel. Compose Multiplatform exists if you want to share UI too. It is a separate, optional layer on top of KMP, so check its current status for each target before committing.
Device APIs. Camera, sensors, Bluetooth, biometrics, and notifications are accessed through platform SDKs, usually behind an interface or expect/actual.
Platform integrations. Widgets, app extensions, deep OS integrations, and anything tied closely to Apple or Android frameworks.

How this differs from "one shared app"

KMP does not mean "write once, run identically everywhere." You decide the boundary. In a typical setup, you might share 30–70% of the code, but that number varies widely with architecture, libraries, and product requirements. Treat any fixed figure as anecdotal rather than a promise.

Why teams consider it

Less duplicated logic. Fewer parallel implementations of the same rules.
Consistency. A pricing bug gets fixed once, not twice.
Native UI and capabilities. You are not forced into identical screens or a non-native UI toolkit.
Gradual adoption. You can start with one module (say, validation or networking) inside an existing app rather than rewriting everything.

Limitations and trade-offs

Learning curve. iOS developers now need to read, and sometimes debug, Kotlin. Android developers need to understand how their code is consumed from Swift.

Interop friction. Kotlin/Native compiles to native binaries, and iOS consumes Kotlin through a generated framework. Some Kotlin features don't translate cleanly to Swift. For example, Flow and suspend functions often need wrappers or helper libraries to feel natural on the Swift side. Swift-facing tooling is still evolving, so check the current state of Swift interop before designing your API surface.

Architecture discipline. Shared code needs clear boundaries. If platform concerns leak into the shared module, you lose the benefit.

Library compatibility. A library must support the targets you need. Many popular Kotlin/JVM libraries are Android- or JVM-only and cannot be used in common code.

Build and tooling. Gradle configuration, source sets, and Xcode integration add complexity. Build times and CI setup deserve attention.

Testing. Common tests run on multiple targets, which is a benefit, but you also need to verify behavior on each platform. Differences in the runtime, especially threading and memory behavior across Kotlin/JVM and Kotlin/Native, can surface issues that only appear on one side.

Team structure. If your Android and iOS teams work independently with little overlap, coordinating on a shared module is an organizational change, not just a technical one.

KMP vs. Flutter and React Native

These tools optimize for different things:

Kotlin Multiplatform    Flutter React Native
Enter fullscreen mode Exit fullscreen mode

Primary language Kotlin Dart JavaScript / TypeScript
Typical sharing model Selected layers (often logic) Whole app, including UI Mostly whole app, UI via native components
UI approach Native by default; shared UI optional Flutter's own rendering Maps to native views
Native integration Direct access to platform SDKs Via platform channels/plugins Via native modules

Neither column is universally "better." Flutter and React Native can suit teams that want a single UI codebase and a single team. KMP tends to appeal when native UI and platform APIs matter, or when you already have Android and iOS apps and want to consolidate the logic underneath. Team skills, existing code, and product needs usually decide it.

When KMP makes sense

You already have native Android and iOS apps and want to deduplicate business logic gradually.
Your Android team already works in Kotlin and can own a shared module.
Native UI and platform features are a core product requirement.
Consistency of rules matters, as in fintech, e-commerce pricing, or offline sync logic.
You want to share an SDK or client library with multiple consumers.

It may be a weaker fit if your team has no Kotlin experience, your app is mostly UI with little logic, or you want a single team owning a single UI codebase. In those cases, evaluating Flutter or React Native is reasonable.

Conclusion

KMP is best understood as an architectural choice about where to draw the line between shared and platform-specific code. Business logic is a natural place to draw it, because it is where duplication is most costly and where platform differences matter least.

A practical way to evaluate it: pick one self-contained feature, such as validation or an API client, move it into a shared module, and see how the tooling, interop, and team workflow hold up before committing further.

Top comments (0)