I wrote the original version of this post in 2020, just after Kotlin Multiplatform Mobile’s alpha release. It’s now reached beta, so I revisited the project to bring everything up to date.
Teams that ship mobile apps for Android and iOS duplicate a staggering amount of code. Two copies of the same business logic, API calls, and local storage. Cross-platform frameworks like React Native and Flutter continue to gain traction in response, particularly at startups that don’t have the time or money to build everything twice. However, these frameworks struggle to match the polish and performance of flagship native apps.
Kotlin Multiplatform Mobile (KMM) aims to deliver the best of both worlds: shared business logic written in Kotlin with thin UI layers for Android and iOS, allowing you to take advantage of the latest and greatest each platform has to offer.

For teams already maintaining separate iOS and Android codebases, Kotlin Multiplatform opens the door to sharing individual features or modules. You can ramp up code sharing at your own pace without the need to rewrite entire screens or introduce a third programming language to your mobile stack.
Kilometer
I developed a simple app called “Kilometer” to explore KMM. It connects to the Strava API and displays the most recent activities from the authenticated user. The source code is published on GitHub.

The shared code covers all the API interactions, data mapping, and persistence. Each client app applies the MVVM pattern with platform-specific navigation logic, view models, and views. The views are defined with SwiftUI and Jetpack Compose.
I’m not a shared code maximalist. It’s possible to share view models and there are reasonable arguments to do so. However, I’ve found it simpler to draw a line neatly above the responsibilities of the UI layer. With this approach, the project nets out around 75% Kotlin and 25% Swift.
Let’s dig deeper on a few key themes and features…
Getting started
Android Studio’s KMM plugin includes a configuration wizard that will set up a basic project structure. It also generates an Xcode project, allowing you to launch the iOS app in a simulator from Android Studio, or jump to Xcode to edit the Swift source. Practically, I always have both IDEs open to work on KMM projects.

When built for iOS, the shared code is packaged as a framework using a Gradle task provided by the SDK (embedAndSignAppleFrameworkForXcode
). For Android, it behaves just like a regular Gradle module.
For next steps beyond basic project setup, I found it extremely useful to reference community projects like PeopleInSpace.
Foundations
All the core dependencies required to connect to a backend service are available in stable versions: Ktor Client for networking, Kotlinx Serialization for parsing JSON, and SQLDelight if you need to maintain a local database.
The number of libraries available to support multiplatform development has expanded significantly. It now boasts support from popular tools like Realm and Apollo GraphQL, as well as much-needed staples like the new Kotlinx-datetime. I’ve been pleasantly surprised that the community projects in my dependencies file have continued to be actively maintained.
Concurrency
Concurrency is one area where KMM has truly leapt forward in the last year. The original Kotlin/Native memory manager had a steep learning curve. It was easy to write shared code that would run as expected on Android’s JVM but produce a runtime InvalidMutabilityException
on iOS.
Thankfully, the days of freezing objects are over. The new memory manager previewed in 2021 and recently became the default in Kotlin 1.7.20. It’s behaviour closely mirrors Android, with fewer restrictions on sharing objects between threads. This lifts a huge blocker to wider adoption.
Platform APIs
Expect/actual syntax enables calling platform APIs from shared code. In the commonMain source set you can define an expect
class or function, which is an interface to platform-specific actual
implementations in androidMain and iosMain.

The vast majority of shared code I’ve written is platform-agnostic business logic in common source sets, but expect/actual has come in handy for things like persistence, formatting, and logging.
Let’s walk through a simple example of persistence with platform APIs. This expect
class has a string property we want to persist.
// commonMain
expect class UserState {
var userId: String
[...]
}
For Android, we can use SharedPreferences
. Constructors for actual
classes don’t need to match, allowing us to pass a Context
just for this implementation.
// androidMain
actual class UserState(context: Context) {
private val prefs: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
actual var userId: String
get() = prefs.getString("id", "")!!
set(userId) = prefs.edit { putString("id", userId) }
[...]
}
A limitation of interacting with the iOS platform in iosMain is that we only have access to Objective-C headers, not their Swift equivalents. So in this example, we need to use NSUserDefaults
rather than UserDefaults
.
// iosMain
actual class UserState {
private val defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults
actual var userId: String
get() = defaults.stringForKey("id") ?: ""
set(userId) = defaults.setValue(userId, "id")
[...]
}
I also ended up using expect/actual heavily for formatting pace, distance, and time strings. It would be reasonable to assume String.format()
is a Kotlin language feature, but it’s borrowed from the JVM and there’s no native replacement yet. Via expect/actual, it’s still possible to use the JVM formatter for Android, and iOS has an equivalent NSString.stringWithFormat()
.
Interop
Types defined in shared code are automatically mapped for iOS. I opted to use Kotlin’s sealed classes to represent the results of operations on interfaces to the shared code. Although Swift and Objective-C don’t support sealed classes, a type check does the job to handle results as either ResultData
or ResultError
.
sealed class Result<T> {
data class Data<T>(val data: T) : Result<T>()
data class Error<Nothing>(val error: Throwable) : Result<Nothing>()
}
Coroutines supports exposing asynchronous functions from shared code. Suspending functions can be dispatched as usual on Android. In the iOS framework, they simply get converted to callbacks, or starting with Swift 5.5 there’s a second function generated for async await.
let result = try await stravaActivity.activities()
if let result = result as? ResultData<NSArray> {
let items = result.data as! [ActivityCard]
state = .data(items)
}
[...]
It’s pretty clunky casting from Objective-C types like NSArray
, but this is easily isolated to view models and tested.
Exception handling
Kotlin and Swift have a huge amount in common, but the languages take opposite approaches to exception handling. Kotlin only has unchecked exceptions, while Swift only has checked exceptions. Functions in shared code can add a @Throws
annotation to allow exceptions to be handled in Swift.
In practice, I’ve preferred to expose result types that wrap exceptions from the shared code.
Testing
I ended up using Kotest for fluent assertions in the shared and Android sources, but otherwise stuck to standard testing tools. If you’re coming from the Android world, you might miss frameworks like Mockito for native and iOS sources. Luckily, it turns out ChatGPT is pretty good at generating manual mocks from interfaces.
To support testing within the iOS codebase, it’s important to define all the functionality exposed from the edge of the shared code in interfaces.
The future
KMM has been usable in production for some time. I worked at Memrise in 2020 when we were already sharing core offline logic between client apps, and there are huge names like CashApp and Netflix on the early adopters list.
With the new memory manager and beta status, KMM is a viable option for new projects, worthy of consideration alongside twin native apps and cross-platform frameworks. It’s especially compelling for teams of iOS and Android specialists, eliminating duplicate effort but retaining the flexibility of building natively. I’m optimistic for a future where mobile teams share more code.