Multiplatform Projects with Kotlin: JVM, JS, Android Tutorial

Jose Flores
12 min readMay 21, 2018

--

I’ve been thinking about starting a new personal project that could possibly target more than one platform. Specifically I was looking for a solution to share some code between a Java project and an Android project (server & client). Fortunately Android was recently announced as a Platform type for Multiplatform projects.

Which got me thinking,

What the hell is a Multiplatform project ?

I wrote an article going over the high level concepts and general overview. As I was writing that, I was working on setting up a Multiplatform project from scratch and had a few minor challenges that took me an embarrassingly long time to get over so I decided to write this for anyone struggling to setup a Multiplatform project or for anyone looking to set one up quickly. Because I wrote this as I was going through the other article , this article will probably make more sense after that.

Setting up a Multiplatform project

Create the project

IntelliJ IDEA CE 2017.3

My default “Create a new project” dialog looks like the following:

Don’t see these options ? Try updating the Kotlin plugin. Configure -> Plugins -> Kotlin

Notice that there are projects like Kotlin (Multiplatform JVM — Experimental). These are the individual modules (platform or common) that we can start out with. You could setup your project by piecemealing a bunch of these modules and correctly setting up their relationships but there is an easier way for those of us who don’t want to dance with the Gradle.

Kotlin Multiplaform project selected under Kotlin tab

We’ll create a new Kotlin (Multiplatform - Experimental) and name our project MyMultiPlatform.

We’ll start with a common module and two platform modules. We will add the Android platform module after setting these up.

Project Structure

.
├── MyMultiPlatform-js
│ └── build.gradle
├── MyMultiPlatform-jvm
│ └── build.gradle
├── build.gradle
└── settings.gradle

The project is configured with the common module being the content root. The top level build.gradle, settings.gradle and src directory all belong to the common module.

Common module

build.gradle

Notice how common this script is. Besides applying the kotlin-platform-common plugin we can also see that all the dependencies are -common. Common modules can only depend on common libraries or modules.

settings.gradle

rootProject.name = 'MyMultiPlatform'
include 'MyMultiPlatform-jvm', 'MyMultiPlatform-js'

Adds the root project name and includes the other modules.

Let’s expect some things

Create a file named Platform.kt and Common.kt in the demo.multiplat package.

main
├── kotlin
│ └── demo
│ └── multiplat
│ ├── Common.kt
│ └── Platform.kt

You don’t have to add the packages but it makes it easier to work with an Android project if they can share the package structure.

Platform.kt

In Platform.kt we can define some expectations. It expects every platform type to define a class named Platform and for it to have the properties listed.

Also a top level function to get a platform.

Common.kt

In Common.kt we define a top level function named commonSharedCode(…). In this function we’re using a type(Platform) that we have not provided a concrete implementation for.

We also define a main function that will be the entry point for both the JVM and JS module. This doesn’t have to be the case, you can define different entry points for different modules. When we get to the Android platform module this main will do nothing for it but we will see that we can still share common code.

Platform Modules

How does the compiler know about the missing expectations ?

If you look at a platform (jvm or js) module’s build.gradle you’ll notice it doesn’t look all that different from a regular (non-platform) module. In the JS module you’ll see a dependency on org.jetbrains.kotlin:kotlin-stdlib-js and in the JVM module you’ll find the equivalent org.jetbrains.kotlin:kotlin-stdlib.

The Glue:

  • Platform Plugin — apply plugin: [kotlin-platform-jvm,kotlin-platform-js , kotlin-platform-android]
  • Screaming “I am expected by” — dependencies { expectedBy project(“:”) }

Platform Module: JVM

Add a file named Platform.kt with the same package structure inside the MyMultiPlatform-jvm module. You can create the file and path manually or use the IntelliJ’s shortcut shown below.

├── main
│ ├── java
│ ├── kotlin
│ │ └── demo
│ │ └── multiplat
│ │ └── Platform.kt
You can also use the IDE support in Common.kt: Alt+Enter
Platform Module: JVM

Here we provide a simple but an actual implementation using hard coded strings.

We also provided an actual implementation of getPlatform().

Platform Module: JS

We will also provide actual implementations for the js module. Like the jvm module its build.gradle applies a platform plugin apply plugin: ‘kotlin-platform-js’ and opts into providing actual implementations expectedBy project(“:”).

Let’s build

Because the Android build process is so different from the JVM and JS modules we will build what we have now and checkout what we have so far.

Build project or run ./gradle build.

./gradlew build has been more reliable for me but I probably haven’t configured something correctly

/build: JVM

Before we checkout how to run the jar we’ll look at the inputs, classes from both the platform module(left) and the common module(right) and the compiled classes.

Left is our JVM module. Right is the Common module

Looking at the JVM output we notice that there is a CommonKt.class, Platform and a PlatformKt.class. You might guess that one of these “Platform” classes is the expectation and the other is the implementation but that is not the case. This is actually caused by the way the Kotlin compiler handles top level functions and the way we wrote this simple program.

Top Level Functions and Kotlin

Common.kt

If we look at our Common.kt file you’ll notice that we didn’t nest these functions under a class but instead left them as top level functions. When this Kotlin file gets compiled into byte code it actually adds these methods to a class named after the file name(Common.kt = CommonKt) unless you define a file name using the annotation @file:jvmName(“WHATEVER_NAME”).

class CommonKt {    fun commonSharedCode(...)...

So now we know where the CommonKt.class came from and if we look at our Platform class you’ll see that inside that class we defined a Platform class and we added a top level function which should match our understanding of the output classes.

Running the JVM output

We have to know the name of the class that defined main so we can actually run the program. We defined main as a top level function inside the Common.kt file so it should be in the CommonKt class and we have to reference it using its full package name. (demo.multiplat.CommonKt)

We could run the program using the java command by including the kotlin runtime dependency (and any other dependencies) and referencing the main class but there’s obviously a better way to do this.

jvm module: build.gradle

This jar block adds the attribute Main-Class to the manifest that is generated when you build which will allow you to run java -jar on the output jar. We also include all the compile time dependencies.

Run ./gradlew build again you should see a jar output.

$ java -jar MyMultiPlatform-jvm.jar 
Common: Hi! System.out.println(JVM)

/build: JS

After running ./gradlew build you should see this in the JS module.

Output of JS module

Our program is in the .js file named after our module (MyMultiPlatform-js.js) but it has a dependency on the JavaScript version of Kotlin (kotlin.js) which is not included by default in multiplatform projects.

I’m not sure if this a bug or if I’m missing a configuration step but we need a kotlin.js so I created a new Kotlin (JS) project (not a multiplatform project and not the individual platform module).

I added a file and wrote a main function that prints anything.

fun main(args: Array<String>) {
println("hi")
}

Build -> Build Project

Here we see that kotlin.js file is included in the output under a lib directory.

I went ahead and copied that directory into my project. This is almost definitely not a good way to get this dependency but for this demo I didn’t want to spend too much time tracking down what could be bugs (especially on the JS platform (; ) and I knew that I compiled both using the same version of Kotlin.

Running the JS output

Now that we have our MyMultiPlatform-js.js file and the kotlin.js file that we stole from a separate Kotlin JavaScript project we can finally run our program.

I pulled these two files outside of the project and into a separate folder.

├── MyMultiPlatform-js.js
├── kotlin.js
└── ++ index.html

In order to run JavaScript files we have to have an html page that references the scripts.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Console Output</title>
</head>
<body>

<script type="text/javascript" src="kotlin.js"></script>
<script type="text/javascript" src="MyMultiPlatform-js.js"></script>
</body>
</html>

Now we open this html page in a browser and check its console output. The output in this case goes to the console because main function called println.

Adding Android as a Platform Module

There are a couple ways you could do this step depending on whether or not you want to import an existing app or if you want to start one from scratch.

If you were wanting to an import an existing app then you would import the app module into your multiplatform project and then make sure to update the project level build.gradle file with the dependencies needed.

We’ll walk through adding a new Android application. For this step I’m going to use Android Studio to open our multiplatform project and then add a new Application module. Then we will update the project level build.gradle so we can build. Finally we will hook up the glue (platform plugin) and fulfill the common module’s expectations.

Opening the project in Android Studio

We’re going to close the IntelliJ IDE and open the same project in Android Studio.

AS 3.1.1

File -> Open -> Select folder where our Multiplatform project is

Adding the application module

Once the project is loaded we can add the phone module.

File -> New -> New Module

We’ll create a new Phone module with the defaults in the create wizard. At this point we have an Android module inside our multiplatform project and our root project can actually “see” it (settings.gradle file should have included the module name include ‘:myapplication’). But we haven’t declared our Android module as a Platform Type. Before we get to that though we need to fix some of the issues that come up when adding an Android module this way.

Fix Android Module issues

You may have noticed that we get some errors when trying to sync and that’s because the module we brought in doesn’t setup the the buildscript found in the root project build.gradle.

The first error I see is:

Plugin with id 'com.android.application' not found.

This plugin is included in the android gradle plugin so let’s add that dependency to our buildscript.

Top Level build.gradle

Now you should see a different error message (progress!).

Could not find com.android.tools.build:gradle:3.1.0.
Searched in the following locations:
...

We should add google() to our repositories block under the buildscript block.

Now when I sync I no longer get the error message about the plugin but rather errors around the dependencies defined in the Android module’s dependency block.

To fix this I added a repositories block in the Android module’s build.gradle.

repositories {
google()
mavenCentral()
}

At this point you should be able to do a gradle sync without errors. As a sanity check let’s try to run the app by clicking the run button.

Marking the Android module as a Platform type

This is a pretty straight forward process and nearly identical to how we defined the other platform types.

In the android module’s build.gradle add:

  • Platform Plugin — apply plugin: 'kotlin-platform-android' under the other module plugins
  • Screaming “I am expected by” — dependencies { expectedBy project(“:”) }

Now that we’ve marked the module as a platform type we’ll build again and hopefully we see an error about us not implementing the expectations.

Providing actual implementations

This is also going to be identical to the JVM and JS modules. We have to provide an actual implementation inside the same package.

Running the Android module

Unlike the JavaScript or Java modules, the Android project doesn’t care if there was a main defined since that is not the entry point for Android projects. We will instead just call the commonSharedCode from the activity.

I added a text view to the layout and set the text to the string returned from our commonSharedCode function.

Calling common code
Android output

Conclusion

We just created a multiplatform project from scratch that targets multiple native platforms. We wrote common code once and in the common module we also defined some expectations for our platform types. In each platform type we provided actual implementations for the expectations and we were also able to run and see the output of each platform.

I can already see some benefits to having multiplatform projects setup this way. Compile time safety and the fact that the contracts instead of being defined through abstract classes or interfaces can now be defined more directly and without leaving a footprint in the final output.

I am interested on how a solution like this looks for a real world project, specifically in what instances would you choose an interface or an abstract class over the compile time expectation. There is actually a way to use a platform’s library in the common module (setting up an expectation and then using a typealias to use a specific library) and I’m wondering how often this will be used (could be helpful in sharing java libraries while still keeping the common code in the common module). It also seems like it would be easy to convert a platform project that makes use of certain architectural patterns (anything that separates framework/platform dependencies ) into a multiplatform project that makes use of the common module and expectations.

Obviously, because this is a simple example meant only to explore some of the new keywords and mechanisms introduced, there may be a lot of benefits and a lot of disadvantages that we are overlooking but hopefully this is a good introduction to start exploring some of the other pros and cons of using a solution like this.

Code: https://github.com/JsFlo/ExploreMultiPlatform

Documentation:

Articles:

--

--

Jose Flores

A passionate Software Engineer with a focus in Android development and a love for solving challenging problems.