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:
Commands:
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.
Previous Post
Originally published at https://www.brightec.co.uk.