Part 1

MrAsterisco
Aug 3 · 13 min read

To me, one of the strongholds of mobile app development has always been to prefer native development when possible. That is, if a large enough team is available, two apps can exist at the same time on Android and iOS (and more, possibly) that do the same things and support the same operations.

The advantages are many and we do not even need to start listing them.

The disadvantage is only one: the two apps, no matter how much time the team puts into fixing this, will always behave a little differently. It can be small, like a different label or a slightly different error handling policy, but it can be big: a different structure sent to a back end with too loose an input check or wrongly implemented logic. You can work through this with UML diagrams, PowerPoint presentations, or almost any tool, but in the end, there are two minds working on the same concept. They’ll always end up writing something differently.

You can either accept this and rely on your QA to find those differences or you can force the developers to talk to each other and have them do some pair programming on both platforms. Or, you can follow me on my journey of writing a cross-platform library in Kotlin to support the development of a medium-sized app for iOS and Android.


The Target

My example app is split in two: a logic/back end side, called Core (talk about imagination, eh?) and a UI/front end side. My target is to migrate as much as possible of Core into a shared module that can be used across Android and iOS. We are going to call that module… <drumroll> MultiCore.

In this first episode, I won’t put MultiCore into the actual app yet. We’ll see it running in two blank apps to isolate any issue that may arise with a larger codebase.


Creating the Library

The first thing we have to do to start developing a cross-platform library in Kotlin is to create a new multi-platform project. To do this, we have to download the big brother of Android Studio: IntelliJ IDEA. Don’t worry; even if it sounds expensive, there is a free version that suits our needs. You can find it here.

“Mobile Shared Library,” aka. “Welcome to the multi-platform world.”

IntelliJ does a pretty good job configuring our project. We have to input a name and select a location, and it will take care of the rest for us.

The default configuration creates three modules and a test module for each, with a grand total of six modules:

  • commonMain: This is where the real magic takes place. This module contains all the shared logic written in pure Kotlin. Any class in here can be shared across any platform.
  • iosMain: a module that links the Cocoa framework and allows us to write Kotlin code which will work on iOS only.
  • jvmMain: a module that compiles to Kotlin/JVM and links the Android framework. This will work on Android only.

We are going to use all of them to write our library.


Configuration

It would be way too easy to believe that the project configuration is good enough as it is. Unfortunately, IntelliJ is not updated to the latest and greatest of Kotlin cross-platform development. This is mainly because Kotlin progresses so fast that it is completely out of the IntelliJ release cycle.

Anyway, it is easy to bring the project up to date.

First of all, we have to update Gradle to the latest version; IntelliJ chooses version 4.10, which is the first to support cross-platform projects, but some things (as we’ll see in the next episodes) don’t work very well.

To update Gradle, open gradle/wrapper/gradle-wrapper.properties and change the version in the distributionUrl property.

At the time of this article, the latest version is 5.5.1, but you can check for new releases here.

After changing that file, IntelliJ will ask you to reload the project and will download the new version. This might take a while, depending on your connection.

Moving on to thebuild.gradle file, if you’re used to Android development, you’ll find this a bit different than normal. We have the plugins declaration on top, which imports kotlin-multiplatform, we have the usual repositories section, and then we have a new kotlin block.

This block defines two things:

  • The targets of our library, which are basically the destination platforms we want to support. These are set by default to jvm() (aka. Android) and iOSX64("ios") (aka. the iPhone simulator).
  • The source sets of our library, which define our modules and their dependencies. By default, common and android import the Kotlin standard library, whereas ios imports nothing.

This configuration is wrong for a number of reasons:

  • First of all, we are only supporting the iPhone simulator, which makes sense for a simple test but is not suitable for a real application.
  • Secondly, we are not taking advantage of the preset configurations that Kotlin provides.

Change the build.gradle file as follows:

The differences are in the kotlin block: we are now configuring targets starting from presets; also, we are selecting the iOS architecture based on the SDK_NAME environment variable, which is set during compilation by Xcode. This way, we will be able to build a framework that works on the architecture we are going to run without changing the buildscript every time.

IntelliJ will warn about a lot of issues in the Gradle file, but it will compile just fine.

Since we’re here, we may as well prepare everything for Xcode. Let’s integrate this Gradle task at the bottom of our build.gradle file:

This task, as described here, builds the native library and packs it into a .framework directory. This also generates a gradlew executable that can be used by Xcode to build the framework together with the iOS project. We’ll use another solution, though, because using the generated Gradle wrapper requires us to run this task manually at least once before we can build everything from Xcode.

When you sync Gradle with the changes, you may notice a green message from IntelliJ, warning you about some modules being removed from the project; this is correct, because we have renamed the default “JVM” target to a more understandable “android.” To make this work, we also have to update the names of the actual folders in the filesystem:

Everywhere you see “jvm” in the “src” folder, just change it to “android.”

Writing Some Code

This is it! We are ready to start writing some code in our module. But to test if it works, we don’t need to: we just have to use the Sample class that IntelliJ has generated for us.

The example defines a hello() function that prints Hello from <platform name> , which varies based on the platform it’s run on. This uses a lot of Kotlin multi-platform specific commands, which we’ll see in the next episode, but feel free to look around.


The Android Project

We’re going to integrate this library into a new Android app, so start Android Studio and open a new project.

An empty activity is exactly what we need.

To easily share the library among the iOS and the Android team, I have created a Git repo for the cross-platform library and have imported it as a submodule into the existing Android repo. You can do this via Terminal or in your favorite Git client. More info on submodules is available here; if you do not have a favorite Git client, you can get Fork, which is a very good free client that also supports submodules.

Once you have imported your submodule, you can include it in your main project as you would with any other external module.

Add this to your settings.gradle file:

You should change the path written after settingsDir to point to the folder where you cloned the submodule.

You could also include the library without creating two separate Git repos, but you will be forced to have a single repository for both apps, in to have everything under source control. In my opinion, this is not ideal; depending on the number of people working on the same repo, potentially changing the same files in the cross-platform project, you might get weird merge conflicts.

Plus, if one team is faster than the other, having a single repo means that once the cross-platform project is updated to support a new feature, both apps must be updated as well at the same time. Otherwise, the one left behind might not be able to build. As you can imagine, this is not ideal in a CI/CD environment.

Android Studio will kindly ask you to perform a Gradle sync; it will fail.

“Plugin request for plugin already on the classpath must not include a version”: crystal clear, isn’t it?

If you click “Open File” in the errors log, Android Studio will open the build.gradle of the cross-platform project at line 2, where we have imported the kotlin-multiplatform plugin. As you can see, there is a version number written there, and Gradle doesn’t like this.

When we were editing our project in IntelliJ, we were using a single Gradle script: the one in our cross-platform project. But now we are trying to import the project as a module of another one, so we are embedding the cross-platform Gradle script into the Android one. This means that we are inheriting all the classpaths and plugins that we have included in the Android project, and Kotlin is also among them with its own version.

Luckily for us, someone has managed to work around this: on the Gradle forum (here) you can find a pretty useful function that applies the kotlin-multiplatform plugin only if it hasn’t been applied before.

So, remove the plugins block in the first part of your build.gradle file and make it look like this:

This works as follows:

  • When building the library in our Android project, the Kotlin plugin is already there; the Kotlin version that you are using for the Android code is going to be used for the cross-platform project as well.
  • When building the library from Xcode, our script will automatically apply the Kotlin plugin at the version we have specified in ext.kotlin_version above.

If you sync your project once again, it should work perfectly now!

Gradle is happy now!

We have just one more step: now that the cross-platform is included in our main project, we have to tell Gradle that our app needs it as a dependency. This is as easy as adding this line in the Android app build.gradle among the other dependencies:

implementation project(path: ':multicore')

Unleash the Power

Now that we have the library in our Android project, let’s start the MainActivity and call the hello function that IntelliJ has defined for us in the cross-platform project.

We can modify the activity_main.xml layout a little to add an ID to the “Hello World” label, then call the hello function in the onCreate method in MainActivity.kt and set the returned value to the content of our TextView.

The hello() function from the sample package!

Build and run, and this is what you get:

You can feel the power, right?

The iOS Project

What we have done so far is just adding a Kotlin library to a Kotlin project; it doesn’t sound complicated, does it?

It is now time to take things a step further and use our cross-platform library in an iOS app. Let’s open Xcode and create a new iOS app.

A Single View app is exactly what we need.

Let’s add a UILabel to the default view controller in the Main.storyboard file and connect it to an IBOutlet in our view controller implementation.

You should also set up AutoLayout to have the text field centred in the screen.

Now that our UI is ready, we should get our hands dirty and put some Gradle in our Xcode. The first thing to do is to clone the submodule into our iOS repo. Then we have to set up Xcode to import the Kotlin library.

Remember the packForXcode task that we added to our build.gradle earlier? It’s now time to use it. We have to run that task to generate a .framework folder that can be imported into our project. But to run that task, we need a Gradle instance …

Working on Android, we are used to always having a configured Gradle ready in our root folder, under the gradlew executable. Obviously, Xcode hasn’t generated a wrapper for us. In their guide, JetBrains suggest running the task once from Android Studio or IntelliJ, then using the generated gradlew in Xcode. I dislike this solution because it means that when cloning the repo for the first time, everybody has to open the submodule in Android Studio and run the task once to build successfully with Xcode. Also, it complicates the configuration of a Continous Integration solution.

I’d rather install a Gradle instance and use it from Xcode. This is easy to do with Homebrew: if you haven’t installed on your Mac, you should check it out.

Gradle requires Java 1.8 or later. Homebrew will return an error if you don’t have it installed and will propose you OpenJDK through Cask; you can also download a normal installer from the Oracle website.

Gradle up and running!

Don’t confuse this instance of Gradle with the one that you normally use when you work on Android. IntelliJ and Android Studio bundle their version of Gradle and, by default, use that even if you have another version installed. Plus, when a Gradle wrapper file is available, both IDEs will always prefer that to any other version. You can, but shouldn’t, change this behavior in the IDE settings.

Now that we have Gradle available, we can configure our Xcode project to run the Gradle task while building.

  • Select the iOS project from the Xcode navigator and click “Build Phases.”
  • Click the “+” in the top-right corner of the view and select “New Run Script Phase.” This will create a “Run Script” item at the bottom of the list.
  • Open the new phase and input the following script, making sure to change the path after cd to the actual path to your submodule relative to ${SRCROOT} (which is the root folder of your iOS project):
  • Rename the “Run Script” phase to something more understandable (such as “Gradle”) and then move it to the top of the list, just after “Target Dependencies.”
  • Build the project!
Xcode is running Gradle!

The first build will take longer because Gradle has to open up an environment, download all the dependencies, and build the Kotlin project. Subsequent builds will be faster because Gradle keeps the environment ready in memory.

If you go and look into the cross-platform project at build/bin/ios/MultiCoreDebugFramework/ you will find our .framework file (with the debug symbols!) ready to import.

This was very easy, wasn’t it?

Now, just like we told Gradle in our Android project to include the cross-platform library as a dependency, we need to tell Xcode to include that framework in our app:

  • Select the iOS project from the Xcode navigator once again.
  • Switch to the “General” tab.
  • Scroll down to “Embedded Binaries” and tap the “+” button.
  • If you have included the cross-platform project as a submodule, Xcode will automatically find the .framework file and put it in the list; if not, you can click “Add other” and select it manually. In any case, make sure to turn off “Copy items if needed.”
There’s our framework right there!

If you chose the framework using “Add other,” there is an additional step: Xcode does not know automatically where to look for frameworks when building, so even if you added the framework correctly, you’ll get an error.

  • Open Finder and navigate to the cross-platform project and then to build/bin/ios .
  • Select the MultiCoreDebugFramework folder and take a note of the path.
  • Go back to Xcode and switch to the “Build Settings” tab and make sure to remove the “Basic” filter by clicking on “All” in the top bar.
  • Use the search field to filter for “Framework Search Path.”
  • Double click on the empty space on the right and press the “+” button in the popover that appears.
  • Input the relative path from ${SRCROOT} to the MultiCoreDebugFramework folder.
  • Build once again; if you input the correct path, the build will end successfully.

Unleash the Power Once Again

Now that we finally have the framework included in our project, we can use it just like we did on Android.

Open ViewController.swift and start by importing the MultiCore framework.

Xcode knows our framework!

As you can see, the Kotlin compiler has generated a module for us that Swift sees. We cannot call the hello function directly, though.

Kotlin functions not in a class are not directly converted to Swift functions. To access the hello function, Kotlin generates a SampleKt class, which exposes that function as a static method.

So our ViewController.swift file becomes something like this:

Build and run and you’ll get this:

Now I’m sure that you can feel the power!

Conclusions

We’ve successfully created a Kotlin cross-platform library and have included it in a new iOS app and a new Android app. There is a long list of things that we can do and explore now, but this episode is already long enough.

I have also created a companion repo on GitHub that contains a slightly modified structure to the one in this article, but it’s useful to test the Kotlin multi-platform technology in an already-configured environment.

Episode 2 is now available!

Better Programming

Advice for programmers.

MrAsterisco

Written by

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade