Platform-Specific implementations in Kotlin Multiplatform

Simone Civetta
Publicis Sapient France
4 min readMar 22, 2019
Photo by Devon Rogers on Unsplash

In our last article, we covered how to structure your Kotlin project in order to support the Multiplatform features. However, in order to share portions of code between platforms and to define platform-specific implementations, we need to leverage some additional configurations and some interesting language features which we’re going to show in this post.

The first one allows us to define how Kotlin modules may depend on each other while a second one tells how code from a given module can interoperate with implementations coming from other Kotlin modules of our Multiplatform project.

In order to fully understand this post, we recommend reading our previous post on the basics of Kotlin Multiplatform.

Defining a Platform-Specific implementation

While sometimes your Kotlin multiplatform project might completely be OS-agnostic, in most cases you need some kind of platform-specific implementation. Examples include using the File System, writing with a Logger, accessing the camera or the device geolocalization. In those circumstances, your code needs some degree of interoperability with other APIs, either provided by the OS or by your app.

In order to allow such interoperability, Kotlin Multiplatform provides us with two options:

  1. Interfaces
  2. expect/actual mechanism

In the next paragraphs we’ll be showing a detailed usage of those features.

Before we start, let’s assume we have a service, named CircleService, which needs to output a log message:

package com.simonecivetta.servicesclass CircleService {
fun circumferenceFor(radius: Double) = 2 * kotlin.math.PI * radius
.also { /* How can we log? `print()` would work, but have we got anything better? */ }
}

Interfaces

A Kotlin interface, no surprises here, corresponds to a Java interface. What’s more interesting, though, is that Kotlin/Native, thanks to its interoperability features, maps Kotlin interfaces to ObjC protocol. And that happens by default!

With respect to the interoperability with other code or frameworks, interfaces can turn out to be quite a powerful feature, as they allow our Kotlin codebase to be injected with any entity, as long as it matches a given interface.

The solution consists in defining a LoggerService interface in Kotlin, such as this one:

interface LoggerService {
fun log(message: String-
}

Our CircleService will then be initialized with the logger.

package com.simonecivetta.servicesclass CircleService(val loggerService: LoggerService) {
fun circumferenceFor(radius: Double) = 2 * kotlin.math.PI * radius
.also { loggerService.log("circumferenceFor($radius): $it") }
}

We can then implement the LoggerService interface in our Objective-C or Swift app. Quite surprisingly, thanks to its interoperability with Objective-C, Swift can transitively interoperate with Kotlin.

Our implementation of the LoggerService would be as follows.

// From Swift...
class SwiftLogger: NSObject, LoggerService {
func log(message: String) {
NSLog(message)
}
}

Please note that since Swift is interoperable with Kotlin through Objective-C, our SwiftLogger class should actually subclass NSObject in order to implement the LoggerService interface.

It’s now time to inject SwiftLogger into CircleService:

// From Swift...
let logger = SwiftLogger()
let service = CircleService(loggerService: logger)
service.circumferenceFor(radius: 10.0)

Et voilà !

Expect/Actual Mechanism

In addition, Kotlin Multiplatform enables further interaction thanks to dedicated keywords, i.e, the expect/actual mechanism.

From the same example:

expect class LoggerService {
fun log(message: String)
}

The actual implementation of the above entity would be implemented in a platform-specific module, for instance in our iosMain project:

import platform.Foundation.NSLog
actual class LoggerService {
actual fun log(message: String) {
NSLog(message)
}
}

Also, please note that actuals can be applied to any top-level declaration, such as global functions.

Alright, which one should I choose?

Aesthetically speaking, these two options don’t differ significantly from one another. Yet, they can make a major difference on your Multiplatform implementation. So, how can you choose the one that best suits your needs? Here are some additional elements that might help you make your choice.

Compile Time Checks

It might seem obvious, but the expect/actual mechanism is checked at compile time: in other words, if an expect is required by another Kotlin project which does not implement it, the compilation will fail. Needless to say, such check is not performed by interfaces.

Language Interoperability

Once again, this one should not come as a surprise: the expect/actual mechanism is a Kotlin feature and you cannot create a Swift or Objective-C module providing an actual implementation for an expected class or function. On the contrary, by using interfaces, you are free to use your language of choice(Swift, Objective-C, JavaScript or Kotlin) to implement the feature for your platform.

All that glitters…

In the examples above, the LoggerService iOS implementation made use of the NSLog Foundation function which is definitely the most widely adopted logging function out there. Truth be told, we could do much better by making use of os_log, a much faster alternative available since iOS 10. Unluckily, at the time of writing, making this API available on Kotlin/Native requires a bit of additional hacking which is beyond the scope of this article. In this case, adding a platform-specific implementation via interfaces brings a clear advantage over the expect/actual mechanism as we would be able to call os_log from Objective-C or Swift.

Conclusion

This article completes our introduction to the basics of the Multiplatform features of Kotlin 1.3. Behind the sometimes lacking developer experience, we feel this project has a good potential and throughout the next months we will be investing more time into digging deeper into the solution.

Find all technical articles by Xebia on blog.xebia.fr

--

--