Blog
Blog copied to clipboard
Thoughts about Kotlin multi-platform
For the past two years, I've been working almost daily on a Kotlin multi-platform (KMM) codebase. In this post I'll try to note down my experience with it. I'll try not to judge whether it's good or bad. Let's analyze instead.
Quick KMM overview
KMM is an SDK from JetBrains that's used to build libraries for use in native Android and iOS projects from a single Kotlin codebase. The idea is very appealing, and simplfies earlier attempts to do that using more complex languages like C and C++. Kotlin is a modern language with very good built-in features, standard library, and a production-grade IDE (Android Studio). Developing a shared codebase should be a joy now, right?
The good parts
I'll start with the good things that should go well as expected.
1. Kotlin is really fun to work with
I'm mainly an iOS developer, so I tend to compare any new langugage I use to Swift. kotlin bears some resemblance to Swift. Optional (nullable) types, immutability (val
vs var
), lambdas (closures), coroutines (async/await), Flows (like AsyncStream
but like with more features from Combine), sealed classes (closest thing to Swift enum with associated values), higher order functions, ...etc. Kotlin doesn't have value semantics though...however, it has data classes which if mixed right with immutability, it can achieve the same goals.
2. Having a shared codebase between multiple platforms enforces modular design
Typically, "business logic" is extracted into the shared codebase, leaving native codebases to deal with mostly UI. Architectures like the Clean Architecture can be easily adopted in such setup, where the domain and most of the data layer can live in the shared codebase.
3. I didn't observe a need for many third-party libraries
Things like JSON serialization and networking over HTTP are provided by kotlinx and ktor which are supported by JetBrains. This enables implementing most of the data layer including the so-called service-level or the area of code that deals with JSON and HTTP.
4. It's good to have a single-source of truth
I was lucky to witness a brief time before adopting KMM in the projects I worked on. A whole class of bugs was eliminated. There were no longer discrepancies between Android and iOS that were caused by a semantic error. However, there were still surprising differences in behavior bewteen the two platforms, but for different reasons discussed later in this post.
The not so good parts
KMM is good, but not perfect. Actually, it can be painful sometimes.
1. Build times...at least for iOS
The Kotlin code is compiled to a binary framework that can be integrated into your Xcode project however you like. As the project grows, having to build and generate a new framework each time you make a change negatively impacts productivity. Notice I didn't include saving time as one of the good things above. Basically the time saved by writing a single code is lost due to the increased build time.
2. Debuggability
Unfortunately, breakpoints that work on iOS are not supported out-of-the-box. There is a community-provided plugin by Touchlab. However, I haven't tried it. And personally, I don't feel comfortable getting used to using a community-provided tool in day-to-day work that may lose support in the future. Huge credit to those who are working on it, but I believe one should use the best and safest tools out there for professional projects.
3. Unusual issues
KMM is in beta as of writing this. I had the pleasure (kidding; far from it) to use it while it was in alpha. Eitherway, it's still not mature enough for issues to surface either in compile-time or run-time gracefully with a meaningful error message. For example, the order of defining variables matter. Consider the following code:
class FeatureDIModule {
private val dependency1 = Dependency1(dependency2)
private val dependency2 = Dependency2()
}
This code will compile fine. However, at run-time, the app will crash with a SIGABRT error at the line accessing dependency2
and that's it. We were lucky to discover that such dependency should respect the order. That is, dependency2
should be declared before its use, like this:
class FeatureDIModule {
private val dependency2 = Dependency2()
private val dependency1 = Dependency1(dependency2)
}
Also, back in the day, before the new KMM memory model, variables shared between threads needed to be frozen even if they're not mutable. Android didn't have that requirement. iOS developers (with fellow QA) had to discover crashes that only happened on iOS. Moreover, coroutines may or may not change threads after a suspension point is reached in a given coroutine. You can think how hard it was to find and fix those crashes. Good news is that this is fixed with the said new memory model.
4. Generated APIs are in Objective-C not Swift
I might be picky here, but I believe I won't be alone. Although you write Kotlin, KMM generates Objetive-C headers for Kotlin code. Effectively, you use the KMM library as if it was written in Objective-C. I think anyone who is reading this knows already how it's not ideal to use Objective-C code from Swift. It's not a deal-breaker, but it's also not ideal.
5. Android knowledge
If an iOS developer enages in the shared codebase, it would be hard to avoid having to deal with Android, even if it's minimal maintenance. For example, a change may break some dependencies on Android. it might be easy to delegate that to an Android team member to take care of the issues. However, I believe it would be better for all parties involved to have knowledge about the other platform to facilitate moving forward. This can be a challenge for a small team.
6. Android-driven design
This may not be technical, but why should we only discuss technical aspects? You may already noticed that KMM makes iOS kind of a second class citizen. While the Android development experience is not affected at all, iOS developers face degraded build times, degraded debuggability, unusual crashes that can be hard to decipher, and clunky Objective-C APIs. But wait, there's more.
It's not a surprise that Android developers will take the lead in designing and implementing whatever happens in the shared codebase. This has undesired impact in my opinion:
- Some iOS developers don't have the capacity to learn Kotlin or even don't want to. Unfortunately this reduces iOS develope into mostly UI developers. Therefore, a team adopting KMM can easily repel iOS talents.
- Even if there are iOS developers involved, Android patterns and principles will probably prevail over whatever an iOS developer will propose. They have to make peace with that :)
- The definition of done can get tricky. Code that is working fine on Android but not on iOS, will likely not be easily contested by an iOS developer having a problem. The most likely outcome will be the iOS developer working around the issue natively if possible. This might sound like a specific team communication problem, but old habits die hard, and it's not odd for a developer to stick to what always worked best, especially if it works alright on their platform.
Conclusion
As I mentioned, no judgments from me. Try it, it may work for you. Personally, I always welcome a new skill. If a team feel the need for having a shared codebase, they might try it in a low-priority project first to have some sense about how things might look like moving forward. And personally, for time and quality sensitive projects, I'd stick to battle-tested boring technologies.