Kotlin Multiplatform is a great way to share code between multiple platforms, but this new approach can be confusing to navigate. There's a variety of sourceSets for platform specific code, plus intermediate sourceSets(such as mobileMain
) that cover multiple platforms at once. One of the difficulties that can arise is how to divide work among your team.
This gets more difficult the larger your team is and the more platforms you're developing for. It sounds straightforward at first: the iOS developer works on the iOS code, the javascript works on the JS code, and so on. In practice however, you may run into unforeseen challenges.
Challenges
- Languages: KMP is of course written in Kotlin, but if you have iOS developers or Javascript developers that only know Swift or JS, then there's going to be a learning curve.
- Android Bias: You may think if you're writing Kotlin code then get the Android developer to do it. Not only are you pushing all the work on your Android devs, but there's chances for Android biases in their code.
- Conflicts: If you have all devs working on their own platforms at the same time, that can lead to conflicts when merging PRs and arguments about the best approach.
- Assigning Tasks: Beyond writing code there is an issue of issue tracking. When assigning work how do you know who's working on what layers? An Android task could include shared code.
So how do you assign work to multiple developers effectively, without causing the team to step on each others toes?
Our Approach
The best approach we've found is to split up the work into two camps:
PlatformSpecificUI
and KMP
For example, if our project is written for Android, iOS and JS we'll have four groups:
androidUI
, iosUI
, jsUI
and KMP
This again may sound straightforward, but there are some things worth mentioning. First let's visualize the layers.
Here we can see the different layers in our example. We have:
androidMain, iOSMain, jsMain, mobileMain and commonMain
So in our example we have the four groups and their work:
-
androidUI works in
androidMain
and the Android layer -
iOSUI works in
iosMain
and the iOS layer - jsUI works in the js layer
-
KMP works in
commonMain, mobileMain, and jsMain
With this approach all the platforms are dependent on the KMP groups work(The common layer).
An Example
So as an example, we want to add a new feature:
A new screen that fetches an image from an api and displays it when a button is pressed. This feature will be on every platform, and it's going to be tracked via an issue tracker.
Above we have an example of what your architecture might look like a lot at first, but let's go over it.
In Android, we have:
Fragment -> ViewModel -> Coordinator -> Repo -> network api
A button is pressed in the Fragment, an action is sent to the ViewModel which using a coroutine calls the coordinator. The coordinator then gets the image from the repository and processes it to bring back to the ui.
A similar flow can be seen for iOS and Javascript, but with their own implementations. In this example we have the interfaces RepositoryCommon
and Api
, which are then implemented platform specific.
Splitting the work
You may be asking why the jsUI
doesn't work in jsMain
, while the other two groups work in their respective sourceSets?
It's better to think of it in this way:
The KMP group is working on the common code and its implementations
In the example above the KMP group is working on the CommonRepository, as well as the implementations. They even work on the next level up(The Coordinator and js Service). The Coordinator and service are still in business logic, and despite having their own platforms they are still using shared libraries.
Why this approach
This approach prevents some of the issues mentioned prior:
- No platform delays: Since all the platform specific work relies on the KMP groups work, there is no one platform that has an extra workload and all platforms start at the same starting line.
- Increasing Platform Knowledge: The KMP group creating the groundwork for all the different platforms means that they can grow their knowledge of all the platforms in development.
- No conflicts: All the platforms relying on the common layer ensures that all the platforms are in agreement with the business logic before working on the platform-specific code.
Note:With this method all platform developers would review and approve this foundational code, to make sure it fits all their requirements. This also helps maintain feature parity across platforms, and avoid platform biases.
Platform Equality
One key issue that I think a lot of KMP developers struggle with is platform equality. It's easy to start with the Android implementation since it's in Kotlin and the KMP library is easily debuggable. This approach puts a focus on Android, and other platforms can lose out because of it. They're seen as afterthoughts and don't get as much focus as Android.
A small example is that lists are not supported in KotlinJS, so if the developer starts using lists in the android implementation they may not notice until the JS project is built.
One of the advantages to having one group work in KMP is that it gives even attention to every platform, as the KMP developer has to write code with all the platforms in mind.
Layer Intermingling
There are some issues that may come up, especially when working between business and UI logic. Developers have different levels of experience in languages and Platforms, so crossing the bridge between logic layers can be tricky.
Examples of these issues are:
-
Platform specifics: shared code can still call platform libraries, such as
iosMain
callingFoundation
, so Kotlin developers may not know these libraries. -
Different Languages: In our approach platform developers are working in Kotlin sourceSets(i.e. an iOS developer has to work in iOSMain). An iOS dev may be familiar with
UIKit
and Swift, but is unsure how to write the code in Kotlin.
In cases like this where a there is an intersection between shared code and platform specific code, pair programming is a great option. Not only does it ensure that the code meets both sides' requirements, but it encourages knowledge sharing.
The KMP developer working on the shared code can create a base implementation, then work alongside the platform-specific developer to find the best approach to the issue. All the while both developers are sharing their knowledge of Kotlin and platform-specific code.
Downsides
The main downside to this approach, is that the KMP dev cannot easily test their implementation. For example if they are writing a network call and tries to serialize it into what they think the format is, it's only until one of the platform developers implements the call that they will realize it's incorrect.
One way to get ahead of this issue is to create tests whenever possible. Testing network layers can be tricky, but writing some form of test to make sure your code works for the platforms will help nip these issues in the bud.
If there is an issue like this, then it should be communicated with the team so that the other platform developers know that there is an issue, and the team can work to resolve the bug.
Conclusion
Every team is different, and every project is different. While this approach may not suit everyone, at Touchlab we have found that it can help organize work and increase knowledge sharing.
Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @kevinschildhorn on Twitter, or on the Kotlin Slack. And if you find all this interesting, maybe you'd like to work with or work at Touchlab.
Top comments (1)
Thanks a billion! Been wondering how to distribute task between common/shared code in the KMP world. This suggestion gaves us a clear ideas