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:
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
.
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
.
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, 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, 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
class Container<T>(private val value: T) {
fun get(): T = value
}
transforms to
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
)
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
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:
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 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 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.
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.
iosMain.dependencies {
// ...
api("org.example:exported-library1:1.0")
api("org.example:exported-library2:1.0")
}
This very well explained in Kotlin documentation
Provide Kotlin API that doesn't make you invoke coroutine inside coroutine
The Kotlin/Native memory manager has a restriction on calling Kotlin suspending functions from Swift and Objective-C from threads other than the main one.
For example if you have Kotlin class
class Client {
suspend fun getToken(): String
suspend fun invokeSmth(token: String)
}
And planning to use it in swift like this:
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
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:
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.
Top comments (1)
FYI, KMM is stable now
touchlab.co/kotlin-multiplatform-i....