KMM: writing Kotlin API for Swift - 7 things you need to know
Tags: kotlinmultiplatform, kotlin, swift
Published: 2023-02-05 12:22:23

_This article is true for Kotlin 1.8_

**Kotlin Multiplatform Mobile** (KMM) has been in beta for a while, first stable release is coming, API is safe to use and that's a perfect time to give it a try!

**KMM** has powerful bidirectional interoperability with Objective-C/Swift (kudos to **Kotlin/Native**), but it also has some limitations and tricky parts. It's not easy to write Kotlin API for your shared module that works nicely in Swift. I've tried to summarize all the things I've come across so far and learned the hard way. Where applicable I also provided links to documentation to help give you extra context. 

## Consider `abstract class` instead of `interface` in Kotlin API for Swift

Swift Protocols generated from Kotlin Interfaces don't have methods from `kotlin.Any`. When you are writing code in Kotlin you are often working with interfaces and implicitly using methods from `Any`. You can compare instances, print them or for example collect them in `HashMap`. 

This code works pretty well in Kotlin:

```kotlin
interface Vehicle

class Garage {
    private var parkedVehicle: Vehicle? = null

    fun park(vehicle: Vehicle) {
        when (parkedVehicle) {
            null -> {
                parkedVehicle = vehicle
            }
            vehicle -> {
                println("$vehicle already parked")
            }
            else -> {
                println("garage is occupied")
            }
        }
    }
}
```

But in Swift part this `interface Vehicle` is translated to `protocol Vehicle` and can't be treated that way, it doesn't have any kind of `equals` or `hashCode`. 

```swift
class SwiftGarage {
    private var parkedVehicle: Vehicle? = nil

    func park(vehicle: Vehicle) {
        if (parkedVehicle == vehicle) { // Compile error: 'Vehicle' cannot be used as a type conforming to protocol 'Equatable' because 'Equatable' has static requirements
            print("\(vehicle) already parked")
        }
    }
}
```  

Of course you could cast `Vehicle` to `NSObject`.  

```swift
class SwiftGarage {
    private var parkedVehicle: Vehicle? = nil

	func park(vehicle: Vehicle) {
		if (parkedVehicle == nil) {
			parkedVehicle = vehicle
		} else if ((parkedVehicle as! NSObject) == (vehicle as! NSObject)) {
			print("\(vehicle) parked")
		} else {
			print("garage is occupied")
		}
	}
}
```
It will work, but it doesn't look nice.

On the other hand Kotlin classes: regular and abstract maps methods of `kotlin.Any` (`equals()`, `hashCode()` and `toString()`) to the methods `isEquals:`, `hash` and `description` in Objective-C, and to the method `isEquals(_:)` and the properties `hash`, `description` in Swift. Also they conform `Equatable`, `Hashable` protocols in Swift.

In addition you can extend them to adopt and conform to any new protocol in Swift.

That's why it better to provide `abstract class` instead of `interface` where it's possible, when you are creating Kotlin API for shared module.

## Don't rely on generics interface in your Kotlin API for Swift

**Generics** interoperability [can only be defined on classes](https://kotlinlang.org/docs/native-objc-interop.html#generics), not on interfaces (protocols in Objective-C and Swift) or methods. So ideally your Kotlin API shouldn't include generic interfaces. 

## Swift Generic generated from Kotlin Generics has limitations

**Kotlin/Native** has [no direct Kotlin <-> Swift interoperability](https://kotlinlang.org/docs/native-objc-interop.html#usage), it uses Kotlin <-> Objective-C and Objective-C <-> Swift interops to make it works. That's create some limitation.

For example Kotlin and Swift define nullability as part of the type specification, while Objective-C defines nullability on methods and properties.

That's why this class in Kotlin 

```kotlin
class Container<T>(private val value: T) {
    fun get(): T = value
}
```

transforms to 

```swift
public class Container<T> : KotlinBase where T : AnyObject {
    public init(value: T?)
    open func get() -> T?
}
```

in Swift.

Fortunately there is an easy trick to make our `get` function non-nullable. You need to provide non-null type constraint (e.g. `kotlin.Any`)

```kotlin
class Container<T: Any>(private val value: T) {
    fun get(): T = value
}
```

now it will translate as expected.

Another interesting thing with **generics** is that in Swift part they have `AnyObject` constraint (maybe you've noticed it in previous example).

That means that we can't use Swift `struct` with them. It also means if we want to initialize our `Container` class with `String` or `Int` we got compiler error 

```swift
let containerInt = Container(value: 1) // Generic class 'Container' requires that 'Int' be a class type 
let containerString = Container(value: "one") // Generic class 'Container' requires that 'String' be a class type 
```
 
There is a solution for this too. You need to cast Swift values to Objective-C one's:

```swift
let containerInt = Container(value: 1 as NSNumber)
let containerString = Container(value: "one" as NSString)
```

This code will compile just fine.

## Avoid name collision even in different packages

All Kotlin classes and interfaces from shared module get into one Swift module. That's why having two (or more) classes or interfaces with the same name even if they are located in different packages can cause problems. **Kotlin/Native** deals with it by adding `_` to the name in Swift module. For example if you have `Utils` class defined in 3 different packages in your shared module in Kotlin, you get `Utils`, `Utils_`, `Utils__` in Swift. 

It is not very obvious how to match these Swift classes with Kotlin's. That's why it's better to create unique names in Kotlin part.

Another option is to use experimental `@ObjCName()` [annotation](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native/-obj-c-name/) which tells **Kotlin/Native** how to translate names.

## Include 3rd party libraries into shared module

When you are building your app in KMM, sometimes you need to pack some libraries along with project files in the shared module. For example if you are using [Decompose](https://arkivanov.github.io/Decompose/) you need to access it's `Value` class in Swift.

In order to do this, there is an `export` method in your `Framework DSL` in gradle build. If you are using regular framework setup  you need to find `binaries.framework` block in your shared module `build.gradle.kts`. And if you are using `cocoapods` look at `cocoapods.framework` block. In this block you need to add `export` methods with all libraries you want to include.

```kotlin
it.binaries.framework {
    baseName = "shared"
    export("org.example:exported-library1:1.0")
    export("org.example:exported-library2:1.0")
}
``` 

And also make sure that libraries decelerated in `export` are `api` dependencies. `Export` only works with `api` dependencies.

```kotlin
iosMain.dependencies {
    // ...
    api("org.example:exported-library1:1.0")
    api("org.example:exported-library2:1.0") 
}
```  

This very well explained in Kotlin [documentation](https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#export-dependencies-to-binaries) 

## Provide Kotlin API that doesn't make you invoke coroutine inside coroutine

The Kotlin/Native memory manager has a [restriction](https://kotlinlang.org/docs/native-ios-integration.html#calling-kotlin-suspending-functions) on calling Kotlin suspending functions from Swift and Objective-C from threads other than the main one. 

For example if you have Kotlin class

```kotlin
class Client {
    suspend fun getToken(): String
    suspend fun invokeSmth(token: String)
}
```

And planning to use it in swift like this:

```swift
func handleButtonClick() {
    client.getToken { token, _ in
       client.invokeSmth(token: token) { _ in
           print("Invoked!")
       }
    }
}
```

It won't work, app will crash while executing nested coroutine. To prevent this you could wrap calling of second coroutine in `DispatchQueue.main.sync` 

```swift
func handleButtonClick() {
    client.getToken { token, _ in
       DispatchQueue.main.sync {
           client.invokeSmth(token: token) { _ in
               print("Invoked!")
           }
       }
    }
}
```

Or else you could turn on experimental flag `kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none` in your `gradle.properties`. But it's better to avoid this situations by creating additional method in Kotlin part:

```kotlin
class Client {
    suspend fun getToken(): String
    suspend fun invokeSmth(token: String)
    suspend fun getTokenThenInvokeSmth(token: String) {
        val token = getToken()
        invokeSmth(token)
    }
}
```

## When you explore KMM articles, check article date before you read it

There are a lot of articles already has been written about **Kotlin Multiplatform Mobile** for the last 5 years. KMM api has been changed and improved since then. Inevitably some of very popular articles are outdated. 

Good examples are articles about **Kotlin/Native** memory models. If there are advices about freezes and a lot of limitation, they are about old memory model which is deprecated now.