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.
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
- Try more tips from official documentation.
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.
How to create a MacOS project with #Kotlin Multiplatform. @KotlinWeeklyhttps://t.co/w9GGGs4d7s
— Tõnis Tiganik (@tonisives) June 29, 2021