Multi-platform libraries built with Kotlin Multiplatform (KMP)
Akihiro Urushihara
Posted on March 14, 2024
Introduction
Are you building libraries? When I create services, I always try to extract domains as libraries whenever possible. Also, if the library is generally useful, I want to release it as open-source software (OSS). So, what kind of technology stack would you use at that time? That's where Kotlin Multiplatform comes in. Kotlin Multiplatform is a framework that allows you to create things for various platforms using Kotlin. Writing libraries in the modern language Kotlin allows them to be used on various platforms, so you can share code between server and frontend, and it should be very attractive to be able to run in various languages as OSS.
With Kotlin Multiplatform, you can build mainly three types of builds for different environments:
Kotlin/JVM
For JVM environments such as Java and Kotlin
Kotlin/JS
For JavaScript environments such as browsers
Kotlin/Native
For native execution environments such as iOS, MacOS, and Windows
In this way, with Kotlin Multiplatform, you can create libraries that work in the above environments. However, Kotlin Multiplatform can only run libraries made for Kotlin Multiplatform, and for example, you cannot use a library written in Java with Kotlin Multiplatform. Therefore, there are some parts where implementation is not straightforward, such as when the necessary functionality for writing a library is not available and you have to create it yourself. Here, I will introduce some implementation points (I will add more points if necessary).
Implementation Challenges
Desired functionality library does not exist!
There's nothing you can do. However, Kotlin's official provides several libraries, so the scope that can be achieved using them is by no means small. Also, AAkira/Kotlin-Multiplatform-Libraries introduces several famous libraries created with Kotlin Multiplatform, which can be helpful. However, it can be quite disappointing when certain environments are not supported. But let's think of it as an opportunity! Let's become the first person to implement it!
There's absolutely no documentation!
Really, even if you look at the official documentation, you might not understand how to implement it. Well, to be precise, you can implement it, but you don't know how to deploy it. Or, you may not understand how to use Gradle, the build tool. Since there's no specific place to look, you'll have to try your best to search online, so please refer to the project I created.
This library is a Misskey client library that supports Kotlin Multiplatform.
It depends on khttpclient and internally uses Ktor Client
Therefore, this library is available on Kotlin Multiplatform and platforms supported by Ktor Client
The behavior on each platform depends on khttpclient.
Usage
Below is how to use it in Kotlin with Gradle on supported platforms.
If you want to use it on Apple platforms, please refer to kmisskey-cocoapods.Also, for usage in JavaScript, please refer to kmsskey.js.
Please refer to the test code for how to use each API.
This library is a client library for Misskey, a Japanese social networking service. You can easily access Misskey's API using this library. The code above is Kotlin Multiplatform code, which can be run in Kotlin/JVM environments. For Kotlin/Native's iOS and MacOS, you can use kmisskey-cocoapods built from the kmisskey library and install it via Cocoapods. For Kotlin/JS targeting JavaScript, you can use kmisskey.js and install it via npm. The build methods for each are described in Github Actions, so please check them along with Gradle.
(Reference) How can the library be used?
The kmisskey library created with Kotlin Multiplatform can be executed with the following code. One of the charms of Kotlin Multiplatform is that you can run similar code on almost any platform.
There are some challenging aspects to coroutines in Kotlin/JS for browsers. The biggest challenge is that the runBlocking function cannot be used in Kotlin/JS browser builds.runBlocking roughly converts asynchronous code execution to synchronous execution, and within runBlocking, you can execute asynchronous code (suspend functions). However, this conversion to synchronous execution cannot be done for browser environments that operate on a single thread, and writing runBlocking in the code will result in an error.
So, you might think, why not just provide asynchronous functions (suspend functions) as a library? However, suspend functions do not support the @JsExport annotation, which explicitly exports them in JavaScript, and these functions cannot be represented in JavaScript. Also, suspend functions are output in a slightly cumbersome form when used in Java, so it may be simpler to provide them as blocking functions in the library. (This is a personal opinion.)
On the other hand, Kotlin/JS provides the Promise class for asynchronous execution, which corresponds to JavaScript's Promise, and by returning this model, you can return asynchronous processing in synchronous functions. Since this Promise class is from JavaScript, you can proceed with processing as usual using functions like then.
However, how can the same code return data itself in Kotlin/JS and return a Promise in Kotlin/JS? You might wonder. In Kotlin/JS, there is a special type called dynamic, which, like JavaScript, accepts any type without causing an error. Using this dynamic type, let's rewrite something equivalent to runBlocking for Kotlin/JS.
At this point, it's important to note that the Promise returned with dynamic is not the original type T, so if you touch the value of this Promise incorrectly, it will result in a runtime
error! Therefore, make sure that functions returning this Promise always return the value itself in the library. Also, in Kotlin/JS, since TypeScript type definitions are included, you need to rewrite the definition to a type wrapped with Promise.
Conclusion
Writing libraries with Kotlin Multiplatform is quite fun and great! When it comes to Kotlin Multiplatform, it often focuses on use cases like Kotlin Multiplatform Mobile, where iOS and Android apps are packaged together, and it's not often seen to create shared libraries. So please challenge yourself! And if you release that library as OSS, please let me know!