The Precarious Problem of Kotlin Multiplatform on iOS

A story of complexity, crashes, and distribution woes

Kris Wong
VMware 360
5 min readMar 9, 2021

--

Photo by Michał Parzuchowski on Unsplash

It seems obvious that the Android developer community would be quick to sing the praises of a new cross-platform technology delivered by JetBrains. I too am in this camp. Meanwhile, the iOS developer community remains skeptical. I’m here to tell you they have every right to be.

I don’t mean to imply that Kotlin Multiplatform Mobile (KMM)is not a “good” solution for cross-platform mobile development. In fact, I believe it to be the best solution available for many teams. However, iOS developers will have a second-rate experience with this technology. There’s simply no way around that at the current time. Allow me to expand on some of the major pain points that we have encountered.

Concurrency and Complexity

If you have worked with concurrency with Kotlin/Native, then you already know where this is going. The concurrency story with Kotlin/Native is simply not ready for production. The JetBrains team is aware of this situation.

This post is a great overview of the memory model enforced by Kotlin/Native when concurrency is involved. The JetBrains team designed this memory model in an attempt to reduce many of the issues that we encounter once we start working with concurrency. However, in practice, it creates so much complexity that it becomes nearly impractical to manage it.

In terms of what needs to be frozen, when it needs to be frozen, how we work with mutable state, and even when things get frozen automatically, it quickly becomes overwhelming. In fact, the JetBrains team themselves has had trouble handling this complexity effectively. In my experience, each new release of the Ktor framework introduces new crashes on Apple platforms. Even when they manage to fix the existing crashes, they also introduce new ones.

I am not here to tell you this can’t be managed — it can. We’re managing it in our projects at VMware. You just need to be aware of what you’re getting yourself into. It’s not at all unusual for an Android developer to push a change that causes crashes on iOS. A comprehensive test suite — which includes tests that exercise code from a non-main thread, are a necessity. Meanwhile, the JetBrains team is currently designing a new memory model to reduce this complexity. You can read about this effort in this post. You should also check the Kotlin/Native section of the roadmap. Specifically, this issue is related.

And, the last point on this specific topic — the stable versions of the kotlinx.coroutines libraries do not even support concurrency at all on Apple platforms. Only the main thread is supported. There are “alternative” artifacts that need to be used to support worker threads. These alternative artifacts have version numbers that end with “-native-mt”, short for native multithreading. For instance, the current version is “1.4.3-native-mt”. See this issue for all the gory details.

No Support for Swift

Kotlin/Native does not support Swift, only Objective-C. This includes both inputs (dependencies) and outputs. The tool used to support Apple frameworks as dependencies of your KMM projects is called Cinterop. Cinterop can only generate bindings from Objective-C headers, so these bridge headers must be available for any Swift framework you want to use as a dependency. Only the APIs exposed through these headers are supported.

Similarly, the framework that gets built by the KMM toolchain uses Objective-C header files. It does annotate them for more convenient naming in Swift, but that is not the same as having a pure Swift API.

Limited Support for Native Dependencies

KMM does support CocoaPods dependencies via the CocoaPods plugin and Cinterop tool. Unfortunately, if you have a dependency whose API is dependent on another library, this is not supported. See this issue for more information. Also, C++ dependencies are not supported. A C shim API must be created in order to leverage a C++ library.

Debugging is not Straightforward

Your iOS developers work in Xcode. If they experience an issue in a KMM library, they need to be able to debug in order to troubleshoot. Fortunately, this is possible. Unfortunately, there are a few manual steps involved. First, they need to install this Xcode plugin. They also need to have the source code for the library available locally. Depending on how you are handling distribution, that may already be the case (more on this later). Lastly, they will need to add the library source root as a folder reference in the Xcode project for the app. This makes the source files available to the Xcode debugger. After completing these steps, they will be set up for debugging.

The Distribution Story could be Better

Frankly speaking, this has always been the case for iOS developers. You have Cocoapods, but it is nowhere near as productive as Maven is for Java / Kotlin projects. Swift Package Manager may be a game-changer in this area, but it remains to be seen. That said, you have a few options for distributing your KMM library for use in iOS apps. This article goes into way more detail than I will here, so I recommend you check it out. Your two main options for distribution are:

  • Fat framework
  • CocoaPods

The primary issue with distributing a fat framework is that you don’t really know exactly which version is included in your source tree unless you have a strict process around how the framework file gets updated. It’s also frustrating when you are trying to iterate on changes in your library and run them in your app project (another incentive for a comprehensive test suite). It is very simple to get started with, however.

In my opinion, using the Cocoapods plugin along with git submodules is the most robust solution for distributing your library at this time. This also makes debugging a little easier, as discussed previously. You add your KMM source repo as a git submodule of your iOS app repo, and then reference the generated podspec file from the KMM repo in your Podfile. The primary issue with this option is that it lengthens the build time of your app, but if you’re using CocoaPods, you’re probably already used to that. This option is impacted by this issue, though it is easily worked around.

Summary

In this post, I covered some of the major pain points that I have experienced in using KMM on iOS. This is by no means an exhaustive list. That being said, leveraging KMM now is certainly possible, and it’s not my intention to scare you away. At VMware, we do have multiple KMM modules that are in production. I only want to share with you some of the realities of leveraging KMM in its early days, so you can be better prepared for what the experience looks like. Regardless of whether you decide to pursue a cross-platform strategy in the immediate future, it is my personal opinion that cross-platform development will become a best practice in the long term.

--

--

Kris Wong
VMware 360

Software engineer+architect. Entrepreneur. Real estate investor.