Categories
Architecture JSON Kotlin Kotlin Multiplatform MacOS

Kotlin Multiplatform in a MacOS app

If a Kotlin developer is familiar with her libraries and language paradigms, it might seem superfluous to learn their counterparts in Mac development. There comes in the possibility to use Kotlin Multiplatform(KMP) in a MacOS app, where familiar libraries and language can be used to generate a .framework, which in turn can be used in Swift. This post will describe the process to create a json parsing library with kotlinx.serialization, and how to include it in a Xcode project.

Setup

One setup option could be an Xcode project with Kotlin native project as a subdirectory, which enables easier build and git integration. The Kotlin Multiplatform Mobile plugin only works for iOS/Android, and cannot be used to create the folder structure automatically for MacOS.

1. Create the Xcode project

To include KMP as a subdirectory, the Xcode project needs to be created first.

2. Create the KMP project

Then, IntelliJ needs to be used to create the Multiplatform project

jvm and js targets are not necessary for MacOS target. Common can be used for development, if later expansion to other platforms is planned.

Option 2

If there are plans for adding other KMP targets, like JVM, the MacOS project could be inside the KMP project. Thus making it easier to add new targets later.

3. Build the KMP module as a framework

To build the framework, it needs to be expressed in KMP-s build.gradle.

macosX64("native") {
    binaries {
        framework {
            baseName = "KmpJsonKit"
        }
    }
} 

Now, the module should be built with

./gradlew linkDebugFrameworkNative

4. Link the framework to Xcode

The built framework is located in build/bin/native/debugFramework/KmpJsonKit.framework. It needs to be dragged to Xcode’s Frameworks, Libraries, and Embedded Content, with Embed set to Embed & Sign.

To make the framework build automatically on next builds, a “Run Script” build phase that triggers the module’s compilation, needs to be defined.

cd kmp-json-kit
./gradlew linkDebugFrameworkNative

Tip: use linkDebugFrameworkNative for faster builds. For a release, switch to linkReleaseFrameworkNative

Now, our framework is available for import. It will be re-built automatically when building in Xcode.

Source code

The json parser app

The idea of the app is to read a JSON file in KMP and return it to MacOS, already parsed by kotlinx.serialization library.

Libraries

Currently, KMP libraries give more focus to iOS and Android. Nevertheless, some important packages, like ktor, sqldelight and serialization are also available in MacOS.

In our app, we will use kotlinx.serialization library to parse the json file. Since there is no file reading library, we will use C’s fopen.

Storage

In our app, we will write a json file to user’s home path on app start. Then, when queried from Swift, we will read it, parse it, and return it to Xcode.

We will use fopen to read and write the files

// read
val file = fopen(absolutePath, "r")
// write
val file = fopen(path, "w")

We will use getEnv("HOME"), to get users home directory path. See Storage class here

Json parser

All of the kotlinx.serialization functions are available for native MacOS development. For our use case, we will use a simple Json.decodeFromString<JsonObject>(jsonString)

fun getJsonValues(): Map<String, JsonElement> {
    val jsonString = storage.readFile(filePath)
    return Json.decodeFromString<JsonObject>(jsonString).toMap()
}

See JsonParser class here.

Tests

There is no support for Mockk or Mockito for native target. For this reason, interface/implementation testing method needs to be used. Our JsonParser class uses Storage to read json files. In the tests, we would like to mock what the storage would otherwise read from the filesystem. Therefore, we create the IStorage interface, that both real and test Storage classes implement. The original functions will be used in live:

interface IStorage {
    fun readFile(absolutePath: String): String;
    fun writeFile(absolutePath: String, text: String);
}

class Storage : IStorage {
    override fun readFile(absolutePath: String): String {
        val file = fopen(absolutePath, "r")
        // etc, real methods
    }
}

and a Mock storage will be used in tests.

class StorageMock : IStorage {
    override fun readFile(absolutePath: String): String {
        return testJson
    }

    // etc
}

val testJson = """
    {
        "testOneKey" : "testOneValue",
        "testTwoKey" : "testTwoValue",
        "testThreeKey" : "testThreeValue"
    }
""".trimIndent()

See the JsonParser test here.

Currently, native tests results are buggy, not returning the correct line. Consequently debugging with println() is sometimes required.

One option to use mockk and Java debugging would be to write the logic in a common module, and then test it in a jvm module.

Framework build speed performance tips

Each additional layer for the build process will increase compilation times. Nevertheless, with proper mitigation, this time can be reduced considerably.

  • Use debug build (linkDebugFrameworkNative) during development.
  • Add performance improving properties to gradle.properties file.
org.gradle.jvmargs=-Xmx3g
org.gradle.caching=true
kotlin.mpp.stability.nowarn=true

Drawbacks

Since the inception of computer programming, new build tools have continuously been proposed. This is the proof that no process is perfect. Surely this is the case for MacOS apps with KMP backend.

Build time

Building a Kotlin framework is an additional process for the app build. Yet, it is hardly noticeable when following the tips in the “build speed performance” chapter. The total build time for this small app is 5 seconds.

Management of 2 projects

Both of the Swift and Kotlin projects need to be managed. Consequently, if a developer doesn’t know one language, making changes might be difficult. Additionally, the knowledge of both Xcode and KMP/Gradle build systems is required. Certainly this can be obstructing when creating smaller Kotlin modules.

Libraries

There are native Kotlin libraries for database, networking, serialization and more. However, otherwise expected packages like file I/O, are still missing. This can be mitigated by calling straight to Cocoa from Kotlin, or using the expect/actual concept.

Conclusion

It is possible to use Kotlin in a MacOS app and this can be encouraged for a developer who is familiar with the Android language and libraries, and doesn’t want learn their Swift counterparts.

It is not recommended for smaller Kotlin packages, because project setup and later management could in the end yield negative time savings. Thus consideration for this setup should only be given for Kotlin modules that fill a specific, bigger task, like file I/O, Network/Storage or Json parsing.