Multi-platform libraries built with Kotlin Multiplatform (KMP)

uakihir0

Akihiro Urushihara

Posted on March 14, 2024

Multi-platform libraries built with Kotlin Multiplatform (KMP)

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.

Kotlin Multiplatform

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.

GitHub logo uakihir0 / kmisskey

Kotlin multiplatform Misskey library.

日本語

kmisskey

Maven metadata URL

badge badge badge badge

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.

repositories {
    mavenCentral()
+   maven { url = uri("https://repo.repsy.io/mvn/uakihir0/public") }
}

dependencies {
+   implementation("work.socialhub.kmisskey:core:0.0.1-SNAPSHOT")
+   implementation("work.socialhub.kmisskey:stream:0.0.1-SNAPSHOT")
}
Enter fullscreen mode Exit fullscreen mode

Authentication

Using MiAuth, request the URL for users to authenticate as follows.

val misskey = MisskeyFactory.instance("HOST")
val response =
Enter fullscreen mode Exit fullscreen mode

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.

Kotlin/JVM

// Kotlin
import work.socialhub.kmisskey.KmisskeyFactory
import work.socialhub.kmisskey.api.request.i.IRequest
...

val misskey = KmisskeyFactory.instance(
    "https://misskey.io",
    "CLIENT_SECRET",
    "ACCESS_TOKEN",
)

val response = misskey.accounts().i(IRequest())
println(response.json)
Enter fullscreen mode Exit fullscreen mode

Kotlin/Native (iOS/MacOS)

// Swift
import kmisskey
...

let misskey = KmisskeyFactory().instance(
  uri: "https://misskey.io",
  clientSecret: "CLIENT_SECRET",
  userAccessToken: "ACCESS_TOKEN"
)

let response = misskey.accounts().i(request: CoreIRequest())
print(response.json)
Enter fullscreen mode Exit fullscreen mode

Kotlin/JS (JavaScript)

// TypeScript
import kmisskey from "kmisskey-js";
import KmisskeyFactory = kmisskey.work.socialhub.kmisskey.KmisskeyFactory;
import IRequest = kmisskey.work.socialhub.kmisskey.api.request.i.IRequest;
...

const factory = new KmisskeyFactory();
const misskey = factory.instanceUserAccessToken(
  "https://misskey.io",
  "CLIENT_SECRET",
  "ACCESS_TOKEN",
);
misskey
  .accounts()
  .me(new IRequest())
  .then((res) => {
    console.log(res);
  });
Enter fullscreen mode Exit fullscreen mode

Coroutine behavior in Kotlin/JS

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.

// commonMain
expect fun <T> runBlocking(block: suspend CoroutineScope.() -> T): T

// jsMain
actual fun <T> runBlocking(block: suspend CoroutineScope.() -> T): dynamic {
    @Suppress("OPT_IN_USAGE")
    return GlobalScope.promise { block() }
}
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
uakihir0
Akihiro Urushihara

Posted on March 14, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related