What We Learned Using KMM For iOS

Stas Keiserman
Jan 12 · 7 min read

The idea to start using KMM came to us when we were designing the implementation of GraphQL in Fiverr. We wanted to do an MVP that uses GraphQL with Apollo (https://www.apollographql.com/), and we saw that Apollo supports KMM. Incidentally, at the same time we read an article about Netflix using KMM as well, with great success. It seems like everything aligned, so we weighed our options and decided to go for it.

Both Apollo’s KMM support and KMM itself are experimental or in beta, so we knew that we were going to run into things that didn’t work as expected and that there would be a learning curve, especially for the iOS guys who had been terrified of IntelliJ and Kotlin. But it was a challenge we were ready to face.

It was going to be an MVP of firsts, using KMM for the first time and using GraphQL but it was even an obvious choice to want to write it once for both platforms and to leverage the power of multiplatform. In this blog, we present what we’ve learned as iOS engineers who had to take a dive into KMM and grapple with unknown beasts like Kotlin and Gradle.

How our MVP and KMM are tied together

I think, before we delve into our findings, it is important first to understand what our MVP was and how we wanted to use KMM.

Our MVP consists of a KMM module that makes GraphQL requests. The KMM module would expose an interface for the app to use and a response block. The entire network logic, GraphQL handling, models, and Apollo will sit inside the KMM module and the app will be oblivious to how things are done. That way, we could also change the implementation in the KMM module from Apollo to another library without the app caring.

One of Apollo’s advantages is its ability to auto-generate classes based on your GraphQL schema. So, for example, if you had in your schema an object for Order, then Apollo would auto-generate a Kotlin class called Order with all its relevant fields.

The problem was that we couldn’t directly use the Kotlin-generated classes in neither Android nor in iOS because they had funky names and convoluted inner structures that would’ve looked really ugly in the clients’ code. So we decided to (1) create DTOs in our KMM project that were a reflection of models we already had in our app (for example, we already had a model called Order for the REST request) and (2) convert the auto-generated Kotlin classes to our DTOs.

In the end, the clients would work with the DTOs, which were way more readable and comfortable to work with.

IDE, environment setup and Kotlin

To start working with KMM, you need to install Android Studio or IntelliJ to code in Kotlin and build the KMM project. So our iOS engineers had to abandon the safe shores of Xcode and learn how to work with a new IDE and tools. There is a slight learning curve here for iOS engineers, and the installation of the new IDE and environment hasn’t been as straightforward for us as installing Xcode.

Apart from the obvious differences between Xcode and IntelliJ (we all know how touchy developers are about their keyboard shortcuts), there is also learning how to handle Gradle and package management. It took us several days to set everything up (with help from our great Android engineers) and start writing code in Kotlin.

Kotlin and Swift are both high-level languages, and, in general, if someone comes from using Swift, they’ll find it easier to learn Kotlin — but it is important to note that there are more differences than similarities between these two languages, both in syntax and in how things were implemented under the hood (like concurrency, for example). There is a steeper learning curve here, and our iOS engineers had a lot of help from our Android partners. You can learn more about Kotlin vs. Swift here.

Although we got better at using Kotlin, we are still far from being proficient, as the net time we spent working on the KMM project is small compared to working on the app.

Concurrency

One of the biggest differences between Kotlin and Swift is how concurrency is handled, and above all, that concurrency in Kotlin/Native is a bit lacking, to say the least. The current implementation has many drawbacks and is quite complex (JetBrains are currently developing a new memory model, which is in Alpha at the time of this writing).

In addition to all that, the stable versions of Kotlinx.coroutines don’t support concurrency on Apple platforms, and only the main thread is supported. There are alternative libraries that can be used to add concurrency to Apple platforms, and that’s what we’ve done. This required us to write additional per-platform code (leveraging KMM’s ability to access platform-specific APIs).

You can see our wrapper below in all its gory detail:

object MainLoopDispatcher : CoroutineDispatcher(), Delay {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_main_queue()) { block.run() }
}
override fun scheduleResumeAfterDelay(
timeMillis: Long,
continuation: CancellableContinuation<Unit>
) {
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, timeInSeconds / seconds),
dispatch_get_main_queue()
) { with(continuation) { resumeUndispatched(Unit) } }
}
override fun invokeOnTimeout(
timeMillis: Long,
block: Runnable,
context: CoroutineContext
): DisposableHandle {
val handle =
object : DisposableHandle {
var disposed = false
private set
override fun dispose() {
disposed = true
}
}
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, timeInSeconds / seconds),
dispatch_get_main_queue()
) {
if (!handle.disposed) {
block.run()
}
}
return handle
}
}

Distribution

After we finished development in the KMM, it was time to build a framework and add it to our iOS app. Unfortunately, this wasn’t straightforward either, and we had to do some extra work.

At the time of our MVP, you could only build separate frameworks for x86 (simulator) and ARM. So if you wanted a framework that supported both, you had to write a script in Gradle that would merge them and create an XCFramework.

This wasn’t an easy task as there is virtually no documentation and we had to learn how Gradle works (it is way more complex than SPM). Once we had an XCFramework, we created a separate repository for it and created a Swift package that could be easily attached to any project using SPM.

Debugging

iOS engineers work in Xcode. If they encounter an issue in a KMM library, they would like to be able to debug to find the problem. Unfortunately, debugging a KMM library just wasn’t possible for us. Although there is an Xcode plugin created by another company (more info here) that would let you debug Kotlin files in Xcode, we never were able to make it work on Xcode 13, and we weren’t the only ones.

We had to rely on using prints and on the goodwill of the Android team who can debug the library without issues. Obviously, it wasn’t convenient and had a negative impact on our development time.

Swift support and Objective-C interoperability

Kotlin/Native compiler creates the iOS framework, as we mentioned earlier. The Kotlin compiler generates Objective-C bindings for Kotlin’s underlying code. At the moment, Kotlin/Native can generate only Objective-C code, and there is no timeline from JetBrains when they will add Swift bindings. You can read more about Objective-C interoperability here.

The lack of support for Swift creates several problems, but it is even further compounded by the differences between Swift, Objective-C, and Kotlin. We had to create an additional layer that serves as a bridge between KMM objects and our objects. The additional layer had code that dealt with the Objective-C code from KMM and transformed it so our Swift objects could be initiated properly. The same layer also dealt with sending data to the KMM module, which also required some transformations. This adds a certain overhead for each KMM object you want to work with.

Additionally, some things get completely broken, like for example Enums or Generics. Enums that are written in Kotlin are translated to Objective-C as objects with properties that correspond to the enum cases. This creates some really ugly code.

//These are parameters we send back to the KMM module.
//Kotlin primitive type boxes are mapped to special Swift/Objective- //C classes. KotlinInt is a representation of Int? in Kotlin.
var sortByAsKotlinInt: KotlinInt? = nil
var filterByAsInt: Int32? = nil // Represents Int in Kotlin
// Kotlin has interoperability only with Objective-C so you have to use NSMutableArray.
var usernamesAsNSMutableArray: NSMutableArray? = nil
// sortBy is defined as Int Enum in Swift. We have extended Int to // have a method to turn it into KotlinInt.sortByAsKotlinInt = sortBy.rawValue.kotlinInt()// usernames is a Swift Array that we have to convert to Objective-C // array.usernamesAsNSMutableArray = NSMutableArray(array: usernames)// That's how a String Enum was translated from Kotlin to Objective- //C by KMM. Properties correspond to the enum cases.@interface MPFGQLOrdersStatusFilter : MPFGQLKotlinEnum<MPFGQLOrdersStatusFilter *> <MPFGQLApollo_apiEnumValue>
@property (class, readonly) MPFGQLOrdersStatusFilter *updates __attribute__((swift_name("updates")));
@property (class, readonly) MPFGQLOrdersStatusFilter *active __attribute__((swift_name("active")));
@property (class, readonly) MPFGQLOrdersStatusFilter *cancelled __attribute__((swift_name("cancelled")));
@property (class, readonly) MPFGQLOrdersStatusFilter *completed __attribute__((swift_name("completed")));
@end

Wrapping up

In this blog, I covered some of the major issues and challenges we encountered when working with KMM on iOS. I didn’t cover all the things we encountered, but it is enough to give you a perspective on KMM. Despite the issues and the longer than anticipated development time, the MVP was a success, and we had a working KMM module shared between two platforms. We kept working with KMM and it is now used in production with great success.

KMM might still be in diapers, but if you persevere despite all the challenges, you can certainly leverage it. My intent in laying out the issues was not to scare you but to share with you the difficulties in using KMM in today’s state. In any case, I think that if KMM keeps evolving it will become the main solution for cross-platform development and may become the best practice for mobile development.

Fiverr is hiring in Tel Aviv and Kyiv. Learn more about us here.

Fiverr Tech

We make the future of work, work