Kotlin Multiplatform
Imagine that you want to build an application that runs on multiple platforms. e.g., Windows and macOS, or Android and iOS. How should you do it?
The first thing you would probably try is to just build the application in your favorite language, and then re-compile it on each platform that you support. If it was a simple application, and you were using a standard language/library that worked on each of these platforms, you might be able to do this. e.g. your C++ 14 console applications could be recompiled on both ARM and Intel architectures.
Unfortunately, it’s not always that simple. Imagine that you want to add graphics: you would quickly realize that the fastest and most sophisticated graphics libraries are platform-dependent. You can do some amazing things with DirectX, but it only runs on Windows, and doesn’t help you build for macOS or Linux (or iOS or Android).
This is a common problem. A lot of the functionality that you will want to use in your application is tied to the underlying platform — including UI, graphics, sound, networking and so on. This means that any libraries or frameworks that you want to use are also tied that platform. Microsoft C++ developers might have a rich ecosystem of Windows-specific libraries to use, but they aren’t portable to macOS or Linux. SwiftUI is incredible for building macOS applications, but doesn’t work for Windows.
So how to you write sophisticated cross-platform applications?
Option 1. Develop separately for each platform.
One solution is to not bother chasing cross-platform code. Use the best tools and libraries, often the native ones that the vendor provides, and create entirely different applications for each platform. This has the advantage of producing the highest-quality software, but you cannot reuse your code so it’s often a very expensive approach. e.g. You design a mobile app, and have separate projects (and development teams) for Swift/iOS and Kotlin/Android versions of your application.
Option 2: Use technologies that exist on every platform.
Instead of building for each platform, you burely on a runtime environment to exist on each of your platforms. This is the architecture of the Java/JVM, and all web applications (which assume a browser running JS and meeting certain requirements).
This is an extremely successful strategy, which can simply deployment for the developer and customer. However, it faces two main challenges:
- You are restricted to the capabilities that runtime platform offers you. Platform-specific features that require access to native libraries may not be available. This is the situation when writing JVM applications in Kotlin; you are limited in your ability to access anything platform-specific. e.g. advanced graphics capabilities.
- The runtime environment may not exist on your target platform. This is rarely a case when talking about web applications, since standards compliant browsers exist on all major platforms. However, the JVM isn’t always available as a runtime target. e.g. Apple doesn’t directly support a JVM and Just-In-Time (JIT) compilation on iOS: everything needs to be Ahead-of-Time (AoT) compiled, which prevents us from directly deploying Kotlin JVM apps on iOS.
Kotlin/JVM helps to address cross-platform compatibility, but it suffers from these restrictions, like every other JVM language. Kotlin Multiplatform (KMP) is the JetBrains strategy to fix this problem.
What is Kotlin Multiplatform?
Kotlin Multiplatform (KMP) is the Kotlin framework to support compilation and code-sharing across on multiple platforms including Android, iOS, web, desktop, and server-side applications.

This approach works because:
- Kotlin has native compilation for a large number of targets, including Android, iOS, Desktop/JVM and JS. Much of the application code that we write (e.g., business-logic) can be written in Kotlin using the standard library, and compiled for each target.
- Kotlin’s library strategy has been to encourage vendors to produce cross-platform versions of their libraries. These cross-platform libraries further reduce our need to use native/platform-specific libraries.
A common strategy with KMP is to either:
- Build your entire application in Kotlin, using KMP libraries, and use a native user interface toolkit for each platform (~80% code reuse), or
- Build your entire application in Kotlin, using KMP libraries, and use Compose Multiplatform as a common UI tookit (~100% code reuse).
We’ll talk more about Compose Multiplatform below.
What does KMP include?
-
Common Kotlin includes the language, core libraries, and basic tools. Code written in common Kotlin works everywhere on all supported platforms, including JVM, Native (iOS, Android, Windows, Linux, macOS), Web (JS). Common multiplatform libraries cover everyday tasks such as HTTP, serialization, and managing coroutines. These libraries can be used on any platform.
-
Kotlin also includes platform-specific Kotlin libraries and tools (Kotlin/JVM, Kotlin/JS, Kotlin/Native). This includes native compilers for each of these platforms that produce a suitable target (e.g. bytecode for Kotlin/JVM, JS for Kotlin/JS).
KMP allows you to build projects that use a combination of common and native libraries, and which can build to any one of the supported platforms—from the same codebase.
Kotlin multi-platform organizes the source code in hierarchies, with common-code at the base, and branches representing platform-specific modules. All platform-specific source sets depend upon the common source set by default.
Common code can depend on many libraries that Kotlin provides for typical tasks like making HTTP calls, performing data serialization, and managing concurrency. Further, the platform-specific versions of Kotlin provide libraries we can use to can leverage the platform-specific capabilities of the target platforms.
Excerpts from https://www.baeldung.com/kotlin/multiplatform-programming.
For example, in the diagram above, commonMain
code is available to all platforms (leaf nodes). desktopMain
code is available to the desktop targets (linuxX64Main
, mingwX64Main
and macosX64Main
) but not the other platforms like iosArm64Main
.
Creating a KMP Project
To create a KMP project, make sure that the Kotlin Multiplatform
plugin is installed, and then use the New Project Wizard
as shown below. Select the platforms that you wish to include, and Share UI
to use a Compose UI on all platforms.

KMP Project Structure
A KMP project is a Gradle project that contains both shared, and native specific source folders.

Compared to a regular Gradle project, we can identify some differences:
- The main source code structure has been moved to
composeApp
. This contains all of the source code for your project which is managed by Gradle. - The top-level
iosApp
contains an Xcode-compatible project which will build the iOS application using Mac-specific build tools. This is necessary because iOS applications MUST be built using the Apple toolchain. Gradle will handle calling these tools for you, but we need this specific project structure for that to be possible. - The remainder of the application is “standard” Gradle i.e. top-level
build.gradle.kts
,settings.gradle.kts
and so on.
Writing Common Code
Let’s write a cross-platform version of the calculator application that we used at the very start of the course. We’ll define some common code and place it in the commonMain
folder. This will be available to all of our platforms. Notice that this is basic Kotlin code, with no platform specific code included.
fun add(num1: Double, num2: Double): Double {
val sum = num1 + num2
writeLogMessage("The sum of $num1 & $num2 is $sum", LogLevel.DEBUG)
return sum
}
fun subtract(num1: Double, num2: Double): Double {
val diff = num1 - num2
writeLogMessage("The difference of $num1 & $num2 is $diff", LogLevel.DEBUG)
return diff
}
fun multiply(num1: Double, num2: Double): Double {
val product = num1 * num2
writeLogMessage("The product of $num1 & $num2 is $product", LogLevel.DEBUG)
return product
}
fun divide(num1: Double, num2: Double): Double {
val division = num1 / num2
writeLogMessage("The division of $num1 & $num2 is $division", LogLevel.DEBUG)
return division
}
The writeLogMessage()
function should be platform specific, since each OS wil handle this differently. We will add a top-level declaration to our common code defining how that function should look:
enum class LogLevel {
DEBUG, WARN, ERROR
}
internal expect fun writeLogMessage(message: String, logLevel: LogLevel)
The expect
keyword tells the compiler that the definition will be handled at the platform level, in another module. For example, we can flesh this out in the jvmMain module for Kotlin/JVM platform. The build for that platform will use the platform-specific version of this function.
internal actual fun writeLogMessage(message: String, logLevel: LogLevel) {
println("Running in JVM: [$logLevel]: $message")
}
Our goal is to define as much functionality as we can in the commonMain
module, but recognize that we sometimes need to use platform-specific code for the results that we want to achieve.
Writing Common Unit Tests
Let’s write a few tests for our common calculator functions:
@Test
fun testAdd() {
assertEquals(4.0, add(2.0, 2.0))
}
@Test
fun testSubtract() {
assertEquals(0.0, subtract(2.0, 2.0))
}
@Test
fun testMultiply() {
assertEquals(4.0, multiply(2.0, 2.0))
}
@Test
fun testDivide() {
assertEquals(1.0, divide(2.0, 2.0))
}
There’s nothing unusual—we can easily write unit tests against common code. However, when we run them, we get a new window asking us to select a target. Select one or more targets for your tests.
Calling Platform APIs
In some cases, it may be desirable to define and access platform-specific APIs in common. This is particularly useful for areas where certain common and reusable tasks are specialized for leveraging platform-specific capabilities.
Kotlin multi-platform provides the mechanism of expected and actual declarations to achieve this objective. For instance, the common source set can declare a function as expected and the platform-specific source sets will be required to provide a corresponding function with the actual declaration:
Diagram from Baeldung.com.
Here, as we can see, we are using a function declared as expected in the common source set. The common code does not care how it’s implemented. So far, the targets provide platform-specific implementations of this function.
We can use these declarations for functions, classes, interfaces, enumerations, properties, and annotations.
Compose Multiplatform
Compose Multiplatform is a port of the Jetpack Compose libraries to supported platforms beyond Android, including iOS, desktop and web. It’s purpose is to allow cross-platform user-interface development across all of these platforms.
Here’s a drill-in of the shared portion of your source tree.

In the composeApp
portion of the source tree:
commonMain
contains all shared code; most of your source code should go here.commonTest
are shared unit tests; most unit tests should go here.iosMain
,jvmMain
,androidMain
are just platform specific code. By default, these will just be your individual main methods (or equivilants) for each platform. Any other platform specific code can go here.
You do not need to do anything “special” to make this build properly. Call the build target for the platform that you wish (from the dropdown Build menu) and the appropriate build tools will be called.
You can also run any of these targets from within IntelliJ; see the Run method in the IDE. Running an iOS target will build for iOS and launch the Apple iPhone simulator. Similarly, running an Android target will build for Android and launch the Android emulator.