A key strength of Kotlin lies in its ability to be compiled into different source code formats. Beyond JVM bytecode, Kotlin can also be compiled into JavaScript (or WASM) and can leverage LLVM to produce native code. This versatility makes Kotlin a perfect choice for crafting truly cross-platform applications.
Building on this versatility, Kotlin Multiplatform (KMP) is a robust feature of the Kotlin programming language that allows developers to share code across multiple platforms, such as Android, iOS, web, and desktop. By using KMP, you can write common business logic once and reuse it across different platforms, reducing development time and effort.
In this topic, you'll explore the benefits and use cases of KMP and learn how to set up a KMP project, define shared and platform-specific code, manage dependencies, and configure builds for KMP projects.
You may have also encountered the term KMM (Kotlin Multiplatform Mobile). This was simply an earlier label for KMP projects targeting only mobile platforms that has since been deprecated.
Benefits and use cases of KMP
One of the main advantages of using KMP is the ability to share code across multiple platforms. This allows you to write common business logic, such as data models, algorithms, and utility functions, in a shared module and reuse it across different platforms. By sharing code, you can reduce development time, maintain consistency, and minimize code duplication.
Interestingly, the capability to share code extends even to user interfaces. By employing Compose Multiplatform (CMP), a declarative UI framework, you can create UI elements once and use them across multiple platforms. However, we will delve deeper into this exciting topic separately.
KMP is particularly useful in scenarios where you need to develop a mobile app for both Android and iOS platforms. Instead of writing separate codebases for each platform, you can create a shared module that contains common functionality and then write platform-specific UI and integration code for each platform.
Another benefit of KMP is the ability to leverage existing Kotlin libraries and frameworks. Many popular Kotlin libraries, such as Ktor for networking and Kotlinx Serialization for JSON serialization, offer multiplatform support, allowing you to use them seamlessly across different platforms.
KMP is also beneficial for building full-stack applications. You can create a shared module containing common business logic and use it on both the client side (e.g., web or mobile) and server side (e.g., backend API). This enables code reuse, reduces development effort, and ensures consistency between the client and server.
Setting up a KMP project
To get started with KMP, you need to set up a new project that supports multiplatform development. It's best to utilize the Kotlin Multiplatform Wizard to generate a custom project structure tailored to your needs:
Create a template using the Kotlin Multiplatform Wizard: This online tool allows you to select your desired targets (Android, iOS, Desktop, etc.) and specify project metadata.
Download the generated project template: The wizard provides a downloadable archive containing the configured project structure.
Open the project using Android Studio or Fleet: If you're comfortable with Android Studio, open the KMP project like any other Android project. Alternatively, you can try JetBrains' latest IDE, Fleet, which has built-in support for KMP.
Kotlin Multiplatform Plugin: As an alternative to the wizard, you can create a new KMP project directly in Android Studio by installing the Kotlin Multiplatform Plugin. However, please note that the plugin currently focuses primarily on mobile development and may not provide the flexibility to target other platforms like desktop or web.
iOS Development Requirement: Be aware that to run and test iOS-specific code within your KMP project, you'll need a macOS system. While Fleet allows editing Swift code (essential for the iosApp part of your project) on other operating systems, running the code still requires macOS. Android Studio doesn't offer Swift editing capabilities.
Once the project is set up, you will typically see a structure that includes:
A shared module: This is where you'll write all the code that can be shared across all platforms, such as business logic, data models, and algorithms.
Platform-specific modules: These modules, named according to the platform (e.g.,
composeApp,iosApp), contain code that interacts with platform-specific APIs.
The shared module consists of multiple source sets (commonMain, androidMain, iosMain, wasmJsMain, etc.).
A source set is a Gradle concept that refers to a collection of files logically grouped together, with each group having its own dependencies. In Kotlin Multiplatform, various source sets within a shared module can be designated to target different platforms.
Source sets can be categorized into two primary groups:
The common source set, which has a predefined name
commonMainand contains shared Kotlin code.Platform-specific source sets, which include Kotlin code tailored to each target platform (e.g., Kotlin/JVM for
androidMain, Kotlin/Native foriosMain, Kotlin/Js or Kotlin/Wasm forwasmJsMain).
Here's a simplified representation of a KMP project targeting Android and iOS:
MyKMPProject/
├── composeApp/
│ ├── src/
│ └── build.gradle.kts
├── iosApp/
├── shared/
│ ├── src/
│ │ ├── commonMain/
│ │ ├── androidMain/
│ │ └── iosMain/
│ └── build.gradle.kts
└── build.gradle.kts
Within commonMain (inside the shared module), you can define common data models, business logic, and utility functions. For example:
// Shared data model
data class User(val id: Int, val name: String, val email: String)
// Shared utility function
fun isValidEmail(email: String): Boolean {
// Email validation logic
}
Running a KMP project
We'll have the opportunity to discuss setting up an environment for different targets and the specificities of running different target applications. For now, let’s assume you already have an environment set up and want to run Android and iOS targets. Running these can be as easy as choosing the target and clicking a button in Android Studio.
Implementing platform-specific code
While shared/src/commonMain contains the common code, there may be cases where you need to write platform-specific implementations. KMP provides a mechanism called expected and actual declarations to handle such scenarios.
An expected declaration is defined in the shared/src/commonMain source set. It represents a contract that needs to be fulfilled by each platform, specifying the signature of a function or a property without providing the actual implementation. The actual declaration, on the other hand, is defined in the platform-specific source set and provides the concrete implementation for the expected declaration.
Here's an example of using expected and actual declarations to provide platform-specific implementations:
// commonMain
expect fun showToast(message: String)
// androidMain
actual fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
// iosMain
actual fun showToast(message: String) {
UIAlertController(title: nil, message: message, preferredStyle: .alert).show()
}
In the commonMain source set, the showToast function is declared as an expected function, indicating that each platform needs to provide its own implementation. The androidMain and iosMain source sets then provide the actual implementations using platform-specific APIs.
Dependency management
Managing dependencies in a KMP project is similar to managing them in a regular Kotlin project. You can use the Gradle build system to define dependencies for each source set inside shared/build.gradle.kts. In the commonMain source set, you specify the dependencies that are common across all platforms, while in the platform-specific source sets, you include dependencies specific to each platform.
Here's an example of defining dependencies in a KMP project:
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
}
}
val androidMain by getting {
dependencies{
implementation("io.ktor:ktor-client-android:$ktorVersion")
}
}
val iosMain by getting {
dependencies{
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}
}
}
In this example, commonMain includes the kotlinx-datetime dependency, which is a multiplatform library for working with dates and times.
You can only use multiplatform libraries inside commonMain
androidMain includes the ktor-client-android dependency for making network requests on Android, while iosMain includes the ktor-client-darwin dependency for making network requests on iOS.
Keep in mind that the dependencies used in the example are for demonstration purposes only. The actual dependencies you include will depend on your project's specific requirements.
Version catalog
If we check the Gradle files in templates provided by KMP wizard, we will see many dependencies specified with implementation(libs.<some>.<dependency>). What's going on here?
They are using a version catalog to manage versions and dependencies for libraries and plugins. You can find this version catalog in a file located at /gradle/libs.version.toml.
Version catalog files usually contain 3 sections and look something like this:
[versions]
agp = "8.2.2"
androidx-core-ktx = "1.13.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
For example, something that could be written in Gradle without using a version catalog:
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
}
will look like this when using a version catalog:
dependencies {
implementation(libs.androidx.core)
}
libs.androidx.core will refer to the androidx-core-ktx entry in /gradle/libs.version.toml. Fortunately, Android Studio can help us add entries to the version catalog. You can write your dependencies using the string version, and Android Studio will recommend extracting them to the version catalog. If you accept the recommendation, it will automatically make the change for you.
Conclusion
Kotlin Multiplatform (KMP) is a powerful feature that enables code sharing across multiple platforms, reducing development effort and increasing code reuse. By using KMP, you can write common business logic once and use it across Android, iOS, web, and desktop platforms.
In this topic, we covered:
The benefits and use cases of KMP for cross-platform development
Setting up a KMP project and defining common and platform-specific code
Implementing platform-specific code using expected and actual declarations
Managing dependencies for KMP projects
With your new knowledge of KMP, you're ready to start building multiplatform applications that efficiently share code across different platforms. Let's put your skills to the test with some exercises!