Here at Hosco we are always eager to try the latest tech. With the recent appearance of Kotlin/Native and Multiplatform tools we wanted to explore if we could share code between our Android and iOS apps.

While there are many examples of blog articles explaining how to create Kotlin multiplatform apps from scratch, this post will focus on the use of shared libraries that can be integrated into existing codebases.

This is possible because Kotlin Native can generate Apple frameworks that we can import into our iOS app and similarly it can make JVM libraries to use on Android. For us this is a first step to start sharing code between our mobile platforms which are currently independent.

What we built

For our first multiplatform library, we chose to start simple. We would write a wrapper for our internal Runnel API service which we use at Hosco to log user's behavioral data. We had already implemented this separately on both platforms so it seemed a good fit to be able to compare and eventually replace with the shared library. The end goal would be to have a simple API that we can call like this:

runnel.sendEvent(appEvent)

This call generates a JSON payload and sends it to Runnel via an HTTP call. We can use Ktor's awesome multiplatform HTTP client for this and also the kotlinx.serialization library to serialize our objects into JSON data. Both libraries are actively developed by Jetbrains.

Setup

Because of the current experimental nature of Kotlin Native, the initial setup was quite tricky and it was not easy to find the right settings. We'd recommend starting with this official tutorial and also taking a look at our build.gradle file which worked for us on Kotlin version 1.3.31. However, keep in mind that things might have changed since.

Importing the HTTP client and kotlinx.serialization

When importing dependencies in a multiplatform project we will need to add them for each platform. For example if we want to use Ktor's HTTP client we would import it like this in our build.gradle:

sourceSets {  
  commonMain {  
    dependencies {  
      implementation "io.ktor:ktor-client-core:$ktor_version"  
    }  
  }
  iosMain {  
    dependencies {  
      implementation "io.ktor:ktor-client-ios:$ktor_version"  
    }  
  }
  androidMain {  
    dependencies {  
      implementation "io.ktor:ktor-client-android:$ktor_version"  
    }  
  }
}

Again, you can find our complete build.gradle file here.

Coding!

Writing the model layer was relatively painless as we were able to copy the existing Android code. We did have to make a few changes though (basically renaming @SerializedName to @SerialName), because the serialization library on Kotlin Native is a bit different than what we use on Android.

Finally, we use coroutines in combination with Ktor's http client for calls. Here's a simplified version of our post request:

fun sendEvent(event: RunnelEvent, success: () -> Unit, failure: (Throwable) -> Unit) {  
    GlobalScope.launch(ApplicationDispatcher) {  
        try {  
            val requestBody = serializeEvent(event)  
            httpClient.post<String>(API_ENDPOINT) {  
                body = TextContent(requestBody, contentType = ContentType.Application.Json)  
            }  
            success.invoke()  
        } catch (ex: Exception) {  
            failure(ex)  
        }  
    }  
}

Building the iOS framework

The targets section in build.gradle defines how to build the iOS framework. We need to set iosPreset to the appropriate architecture: iosArm64 for real devices or iosX64 if we are building for the iOS simulator.

targets {  
    def iosPreset = presets.iosArm64  
    fromPreset(iosPreset, 'ios') {  // 'ios' defines the name of our gradle task (linkIos)
        binaries {  
            framework {  
                // Disable bitcode embedding for the simulator build.  
                if (iosPreset == presets.iosX64) {  
                    embedBitcode("disable")  
                }  
            }
        }
    }
}

Then we can run gradle linkIos to build our framework. The generated file will be in build/bin/ios/releaseFramework/. For more information check the official tutorial here.

Once built, we can integrate the framework to an iOS project by dragging the file to our "Frameworks, Libraries and Embeded Content" section.

Finally we can import the framework and use it in our Swift code :)

import mobile-runnel
...
let runnel = RunnelService()
runnel.sendEvent(appEvent)

Building the Android library

To build for android we can run gradle publishToMavenLocal. The generated file will be in outputs/aar/ and then we can import it in our Android app as usual using the gradle file.

Final thoughts

Kotlin Native enables us to share code between our two mobile platforms in a way that was not possible before as we can reuse large parts of our existing Android code. The performance on iOS was on par with the equivalent Swift solution.

However due to the experimental nature of Kotlin Native, the setup process is difficult and under constant changes. Ultimately we have decided not to integrate this shared library on our production apps just yet because we prefer to wait until Kotlin Native matures as a platform and we still have some of our own challenges left to solve such as integrating everything with our current CI tools and controlling the increase in app binary size.

We will follow the development of Kotlin Native closely and hopefully we can have some shared code running on our production apps soon!


Our mobile team is looking for new talents! Take a look at our open positions.