Kotlin Multiplatform (KMP)

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 compile it wherever you want to run it. If it was a simple application, and you were using a standard language that worked identically on platforms, you might be able to do this. e.g. the C++ 14 console applications that you built in CS 246 could probably build and run anywhere! You’re using a well-defined language supported across multiple platforms, so this probably works fine.

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 (i.e. hire n teams, where n is the number of platforms).

e.g. You design a mobile app, and have separate projects for Swift/iOS and Kotlin/Android builds.

Option 2: Use technologies that exist on every platform.

Instead of building for each platform, you build for a runtime environment that exists on each of your platforms. This is one of the major justifications for companies targeting the Java JVM: “Write once, run anywhere”, at least for some types of software. This is also one of the main benefits of targeting the web: as long as a compliant browser exists, your webpage, or web-based application can run on every platform.

e.g. Gmail, Netflix and Facebook for web applications; Kotlin applications using cross-platform UI toolkits.

This is an extremely successful strategy for reaching a wide audience, but it faces two main challenges:

  1. 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. The Java ecosystem contains a lot of intrinsic functionality, but you will be limited in your ability to access anything platform-specific. e.g. this is one of the reason why Java is rarely used to develop commercial quality games: the JVM does not provide low-level access to advanced graphics capabilities of the platform.
  2. The runtime environment may not exist on your target platform. This is rarely a case when talking about web applications, since browsers exist on most platforms. However, the JVM isn’t always available. 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 both of these restrictions.

Kotlin Multiplatform (KMP) offers a solution to this problem, by allowing us to produce native binaries for multiple platforms from the same code base. It helps us organize our code into reusable sections, while supporting the ability to interoperate with native code on each platform when required. This drastically reduces the effort required to write and maintain code across different platforms, and lets us bypass the restrictions of other solutions like the JVM.

What is Kotlin Multiplatform?

Kotlin Multiplatform (KMP) is the Kotlin framework to support compilation on multiple platforms. As we’ve seen, Kotlin can already be used on any platform that has JVM support. KMP extends this to native builds on other platforms where a JVM is unavailable or undesireable for some reason. You use KMP to write native code using Kotlin for:

  • Android
  • iOS, WatchOS, TVOS
  • macOS
  • Windows
  • Linux
  • Web

Support for multiplatform programming is one of Kotlin’s key benefits. Kotlin provides common code that will run everywhere, plus native-platform capabilities.

Kotlin Multiplatform
Kotlin Multiplaform organization. Taken from Kotlin Multiplatform documentation.

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 versions of 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). Through these platforms you can access the platform native code (JVM, JS, and Native) and leverage all native capabilities.

“Platform-specific” functionality includes the user-interface. If you want a 100% “native-look-and-feel” to your application, you would want to build the application using the native UI toolkit for that platform. e.g. Swift and SwiftUI for iOS. Cross-platform toolkits that we’ve used in this course, like JavaFX and Compose solve the problem of cross-platform compatibiltiy by providing a UI framework that runs everywhere, at the cost of being somewhat “non-native” feeling.

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.

Code shared across targets

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.

Platform-specific 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.

Creating a KMP Project

Use the online KMP Multiplatform Wizard to generate a project matching the platforms you wish to support.

KMP Multiplatform Wizard

This generates a project with the kotlin-multiplatform Gradle plugin. This plugin is added to our build.gradle file.

plugins { kotlin("multiplatform") version "1.4.0" }

This kotlin-multiplatform plugin configures the project for creating an application or library to work on multiple platforms.

Our project will contain shared, and native specific source folders.

kmp-folders

It also generates source sets and Gradle build targets for each platform.

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.

img

Shared Mobile Code

The most common requirement for KMP is to support both Android and iOS targets. Kotlin Multiplatform Mobile (KMM) leverages KMP to simplify the development of cross-platform mobile applications. You can share common code between iOS and Android apps and write platform-specific code only where it’s necessary, typically to support a native UI or when working with platform-specific APIs.

A basic KMP project consists of three components:

  • Shared module – a Kotlin module that contains common logic for both Android and iOS applications. Builds into an Android library and an iOS framework. Uses Gradle as a build system.
  • Android application – a Kotlin module that builds into the Android application. Uses Gradle as a build system.
  • iOS application – an Xcode project that builds into the iOS application.

Basic Multiplatform Mobile project structure

Kotlin supports two-way interop with iOS: Kotlin can call into iOS libraries, and vice-versa using the Objective-C bindings. (Swift bindings are being developed). Android is a native target for Kotlin, and is much easier to support.

KMP is exciting because we can use Kotlin for both targets, and share probably 50-75% of the code between platforms. The native code is just those modules that are very specific to each platform, typically the UI. A KMM application could potentially offer identical functionality on Android and iOS, while delivering a completely native UI experience with Jetpack Compose on Android, and SwiftUI on iOS.

For example, see the list of KMM Samples.