Adding Multiple Kotlin Multiplatform Dependencies to an iOS App

AKA the “Closed World” phenomenon

Kris Wong
VMware 360
4 min readMar 23, 2020

--

This is the fourth post in a series on Kotlin Multiplatform. In my third post, I walked you through the steps to add an Android target to your Kotlin Multiplatform Project (MPP). In this post, I’ll walk you through the steps to aggregate multiple MPP frameworks into one umbrella framework for your iOS app.

Currently, it is not possible to add multiple Kotlin Multiplatform dependencies to an iOS app. It will crash at runtime. To briefly summarize the issue, here is a quote from a JetBrains employee that was posted on GitHub:

Kotlin/Native follows “closed world” compilation model. This means that the entire Kotlin world is compiled to the single native binary, e.g. framework. Two Kotlin/Native frameworks would mean two different Kotlin “worlds”…

For more information on the issue, check out: https://github.com/JetBrains/kotlin-native/issues/2423.

This post will walk you through the workaround discussed in the comments of this GitHub issue. It’s also your lucky day, because this post is somewhat of a 2 for 1. I’ll be introducing Kotlin Gradle DSL for the first time in the build script examples. If you’re still using Groovy, I highly recommend you switch. The experience is much nicer with Kotlin.

Before we dive in, this post assumes you have some knowledge of Git submodules. If you don’t, there are many articles about the topic on the interwebs, like this one here.

Getting Started

To get started, you’ll want to create a new MPP with IDEA, as described in this post. We will add each MPP dependency as a subproject via git submodules. Obviously, in order to do that, you will have to have initialized Git in your new project directory. If you want to do that piece later, you can copy the source of your project dependencies (or create a symbolic link) in your project directory. In our case, we have two project dependencies added as submodules under the modules directory from the source root. Once you have your submodules setup, it’s time to edit settings.gradle.kts.

Setting Up the Build Files

If you’ve never worked with Kotlin DSL, it’s not too much of a pain to convert from Groovy. Here are the resources I used when converting my build files:

Here is our settings.gradle.kts file:

Let’s break this down. In the pluginManagement block, we are defining our Kotlin version, and specifying to use that version for all Kotlin plugins. We cannot specify the version in the build.gradle.kts, because that will cause an error if the version is specified in both the project’s build file and the subprojects’ build files.

The next two lines are standard, and are included in the new project template.

Finally, we include our two subprojects. By default, the subproject directory is expected with the same name, within the source root. We have placed the projects under a directory named modules, so we must inform the script of where to find them.

So far, pretty straightforward. Now let’s take a look at build.gradle.kts:

Let’s go through this, block by block.

  • The import statements are managed by the IDE, just like normal Kotlin code, so don’t worry too much about that.
  • Within the buildscript block, we are defining a project extra, and setting up the Android plugin. If none of your subprojects are using the Android plugin, you can skip those bits.
  • Setup the Kotlin Multiplatform plugin.
  • Configure your normal maven repositories.
  • Within the kotlin block, we are setting up our targets. In our case, we include three iOS ABIs in our framework.
  • We create one source set named iosMain, which our target source sets will depend on. This source set is dependent on our two subprojects. Notice these are api dependencies, and not implementation dependencies.
  • The next block configures all the targets. We set our source set dependency. We also set our framework name. In our case, one of our subprojects is using cinterop, so we need to link this framework binary when we build our targets. If you don’t have this setup, then you don’t need to worry about this line. Lastly, we tell the Kotlin/Native build system to export our api dependencies, so that their symbols will be available in our framework.
  • The last block in the file configures the fat framework task. This is something we have seen in a previous blog post, only written in Kotlin this time. All we are doing is adding each targets’ framework to a list, and then passing that list to the fat framework task.

Adding Dummy Source

The last thing we need to do for this to work correctly, is add a dummy source file to the iosMain source set. If you don’t do this, Kotlin/Native won’t build any of the targets (apparently it thinks there’s nothing to build). So, under your src directory, create the path iosMain/kotlin. In this directory, create a Dummy.kt file (or call it whatever you want). Within this file you can specify a package name and add the following source:

// Kotlin requires some code to build the targets correctly.
private const val dummy = "dummy"

Wrapping Up

Now you can execute your fatFramework Gradle task, and build your umbrella framework. You will need to create one of these projects for each of your iOS apps, or at least for each unique set of dependencies used by your iOS apps. It’s definitely not ideal, but this is what’s required at this time.

Stay tuned for my next post in this series, where I will discuss publishing MPP artifacts to a Maven repository.

--

--

Kris Wong
VMware 360

Software engineer+architect. Entrepreneur. Real estate investor.