Kotlin Multiplatform
Written by Alistair Sykes
Dec 04, 2019

Kotlin Multiplatform Android/iOS: Project Structure Strategies

How should you structure your multiplatform project?

Kotlin Multiplatform

Kotlin Multiplatform (KMP) is a way of writing cross-platform code in Kotlin. It is not about compiling all code for all platforms, and it doesn't limit you to a subset of common APIs. KMP provides mechanisms for writing platform-specific implementations for a common API.

Android/iOS

One of the common use-cases, and indeed my main use-case, for KMP is to share code between an Android and iOS project.

Options

With KMP you have a few different options when it comes to deciding how to structure your projects.

All in one

You can combine your Android and iOS project into one project. They would live within the same directory and both be able to access and build the KMP shared code.

One of Kotlin's example projects has this structure:

https://github.com/Kotlin/kotlin-examples/tree/master/tutorials/mpp-iOS-Android 

They also have a great codelab which walks you through setting up a project like this:

https://kotlinlang.org/docs/tutorials/native/mpp-ios-android.html

Benefits

The main benefit of this approach is how easy the shared code is to access. No matter which platform you are working on, you can access, update and build the shared code. This makes development fast, particularly for solo development teams.

Drawbacks

This setup can be overwhelming for new team members joining a project. There is a lot of code to digest and understand.

For teams with separate Android and iOS developers, working this way can be difficult. Changes can get made in the shared code, which requires changes on both platforms. Meaning you have to work more in tandem, which can be difficult for resource scheduling.

All separate

You could completely separate out your shared and platform projects. This involves writing build (Gradle) logic to distribute your shared code between projects.

Android Lib

KMP provides a gradle task for building a JVM JAR file for the Android code.

./gradlew [targetName]Jar

iOS Framework

To create the framework needed for iOS you can write a custom gradle task, for example:

task releaseFatFramework(type: FatFrameworkTask) { 
    group = LifecycleBasePlugin.BUILD_GROUP
    baseName = frameworkName 
    destinationDir = file("$buildDir/fat-framework/release") 
    from( 
        kotlin.targets.iosX64.binaries.getFramework("RELEASE"), 
        kotlin.targets.iosArm64.binaries.getFramework("RELEASE")
    ) 
}

This can vary based on your exact target configuration.

Benefits

The code has good separation of concerns and is great for bigger teams. You can distribute the shared code in a more structured manner. Deciding exactly when to release to the two platforms.

For bigger teams, who have separate Kotlin engineers, this might be a helpful approach.

Drawbacks

This approach could result in lots of CI configuration and boilerplate. It will probably take longer to setup.

Having to have multiple projects open will cause frequent context switching during development. It will also take more cognitive load to understand the entire picture.

Middle Ground

The middle ground approach would mean having the shared code within one of the platform projects.

This naturally (and arguably obviously) would fall within the Android project. This isn't necessary, but given it shares a language with Android, this would be my recommendation. It will reduce cognitive load and feel familiar to Android developers.

This will, however, shift the responsibility for the shared code onto your Android team. It is worth considering this point and making sure it's right for your team.

Within the Android project you setup the shared code as a KMP module. IntelliJ has some helpful wizards for this. 

This approach makes depending on the shared code, within Android,  easy:

dependencies {
     // ...
    implementation project(':shared')
    // ...
}

You can then add a framework generation task for iOS, like the one detailed above.

I then integrated this framework generation into our CI strategy. Every evening, if the shared code has changed, we submit a pull request into the iOS project. This pull request updates the shared framework. This means your iOS team will be aware of the update and be able to merge the change quickly. It also fits into the iOS CI process, giving you warning of any test failures and therefore changes that are needed.

We use CircleCI (version 2.1), and this is the job configuration:

Job:

https://gist.github.com/alistairsykes/63140145b05cb77ea0b2d508f23e5b0e 

Commands:

https://gist.github.com/alistairsykes/4887fc09be01461729e3307a01bcec35 

https://gist.github.com/alistairsykes/5b46d851f5dd340c188c47679c09de93 

Environment Variables:

FRAMEWORK_GIT_URL: The url to the git project you would like to commit the updated framework into. e.g. git@github.com:org/Project_iOS.git

FRAMEWORK_PATH: The path to the destination framework directory. No leading or trailing separator. e.g. Project

GITHUB_TOKEN: OAuth token for accessing github through hub command line

Benefits

This approach reduces cognitive load and minimises the setup and boilerplate. It keeps the Kotlin code in one project, so not introducing more overhead to your iOS team.

Drawbacks

Shifting the shared code responsibility to your Android team might not be the right approach for you. It means scheduling your resources differently.

 

Our Approach

We currently use KMP to share the M (Model from MVVM) between Android and iOS. Meaning we share business logic, API integration and local data storage. These concepts don't often require platform-specific variations and shares well between platforms.

We use Kotlin on our Android projects, and we generally have separate teams for each platform. So using the middle-ground approach fits our teams well. We try to schedule our Android work slightly ahead of iOS, to give time for the shared code to be started. This eases the iOS development and reduces the number of blockages.

Top