· 9 min read Posted by Kevin Galligan

Why Kotlin Multiplatform Teams Should Share Source, not Binaries

Virtually all native mobile teams evaluating KMP start with a library publishing model. KMP module libraries are written, versioned, and "consumed" by the apps. The classic approach is to build and publish binary dependencies. In 2024, with KMP at stable and gaining significant traction, we no longer recommend this approach. It makes an inherently imbalanced iOS developer experience worse. Share Kotlin source instead.

TL;DR

Using Kotlin source to publish KMP code instead of binaries allows the iOS team to browse and debug the Kotlin code.

GitPortal provides a library publishing mode, which can “publish” versions based on git tags, just like you would with binaries.

Our post series KMP For Native Mobile Teams presents a full overview of our current approach for native mobile teams evaluating and adopting KMP. To understand how teams adopt KMP, and how to address the challenges teams often face, make sure to read through that series.

The Basics

For native mobile teams evaluating KMP, the most common approach is to publish XCFramework binary dependencies that iOS developers can directly import. This is such a common approach, that we publish KMMBridge to make setting up XCFramework publishing easy¹.

Publishing binaries used to be our recommendation for teams evaluating KMP. However, our recommendation has changed. If your goal is to have the team adopt KMP at scale, we now recommend sharing KMP with source code. The reasons for doing so, and why our approach has changed in 2024, are simple:

  • iOS developers cannot browse or debug KMP code that is published as a binary. If you want your team to become familiar with Kotlin, they need to be able to see Kotlin
  • Android developers new to KMP rarely know how to write Swift-friendly KMP. Presenting KMP as a “black box” binary API makes a bad situation worse
  • KMP in 2024 is clearly “production ready”. You don’t need to prove that it “works”. You need to figure out if it works for your team

To facilitate publishing source code, we’ve also published a tool: GitPortal. It uses various git operations to share KMP modules between repos. Conceptually similar to git submodule and subtree, it is purpose-built for our use case.

Context is important

KMP is a big topic. There are many platforms you could build for, and different ways to apply the tech. For native mobile specifically, the layer at which you’re sharing can be from “some library modules” all the way through the UI.

For professional guidance to be useful, it needs to be specific. When discussing shared KMP for native mobile teams, this is the situation we are discussing:

  • You’re building native mobile apps, with a team of native mobile developers
  • At least one of the devs is an iOS dev, and will be expected to contribute to the KMP at some point
  • Your goal is to have the team write KMP at scale. That is, more than a few specific libraries written and maintained by a subset of the team

If those conditions apply to your situation, you should publish with source code rather than binary dependencies.

Why is source better?

To make it simple: If you want your team to become familiar with Kotlin code, they need to see the Kotlin code. A binary iOS dependency does nothing to accomplish that, and can potentially leave the team with a negative first impression. To understand why, read on.

Browsing and debugging

On Android, AAR (or Jar) dependencies generally have a source jar published with their binary build. Android Studio and Intellij IDEA (or any other reasonable JVM-based IDE) can access the source. This allows the consumer to browse and debug the library itself.

On iOS, you generally need to publish XCFramework dependencies as pre-compiled binaries. There is no “source jar” equivalent. Why? ObjC and Swift dependencies are usually published as source code rather than binary, and the Xcode tooling knows what to do with those. For those languages, the only time pre-compiled binaries are used is for closed-source libraries. The Xcode toolchain does not know what to do with Kotlin, so you need to publish pre-compiled binaries.

Can you just clone the Kotlin source and point Xcode at it? Not if you’re using a pre-compiled binary. This is an LLVM thing, not a Kotlin thing. When a debug binary is built, the metadata for debugging points at the absolute path of the source code. So, if you publish an XCFramework binary built by CI, Xcode will look for source files at the path which the CI server used when the XCFramework was built.

In summary: you need to build the Kotlin locally, and point Xcode at that same local path.

There is a workaround

To be precise, you can debug code built on a different path. It’s too much trouble to be practical, however. In short, you need to provide a path mapping override in the framework metadata. You’ll also need to configure the CI build to use a “known path”. Then the iOS dev needs to clone the Kotlin source, of course checking that the dependency version matches the source code tag/version, then edit the framework metadata to the new local path, etc.

As an alternative, you can just build from source code locally. It’s easier.

iOS Developer Experience

The KMP iOS developer experience is worse than the KMP Android developer experience. Even if the tooling was arguably equivalent, the iOS developer is not familiar with Kotlin. Everything is new. Familiar-ish, but all a bit weird.

As it happens, the tooling quality between KMP for Android vs KMP for iOS is very imbalanced. Most “official” best practice assumes the iOS developer will need to adopt a different IDE to get anywhere near the Kotlin code. The API exported to Swift from Kotlin can be rather bad if the person writing the Kotlin side is unfamiliar with how to write good Swift-facing Kotlin APIs.

The tooling and library choices you make can significantly improve or degrade the out-of-the-box experience.

When using KMP for native mobile, publishing binaries makes integrating the KMP logic easier. However, the iOS developer cannot “see” the code. Worse, if the devs writing the KMP code aren’t experienced writing KMP code for Swift, the API presented will probably be bad. If the goal is to get the iOS team “on board”, that’s not a great start.

Kotlin exposure

Again, with our goal of having the team use KMP at scale, sharing source allows developers to see, debug, and lightly edit (println, etc) the code. You can start here, without the expectation that everybody will start hacking away at the Kotlin, or take time away from their day-to-day work to go to “Kotlin training”. A lot of coding is learned informally. Seeing, playing around, hacking a bit.

Starting with binaries, with their limitations in mind, is just wasted time.

What has changed about our recommendations?

Touchlab started working with teams applying KMP very early on, with some of the first teams attempting to put KMP in production. This has been one of the core parts of our business since 2018. In that time, we’ve seen a lot of KMP integrations.

Most teams start by adding binary framework dependencies. This was our recommendation as well. Check out KMMBridge if you want to quickly set up XCFramework publishing.

A few years ago, KMP wasn’t “stable”. Pushback on KMP was common, and reasonable pushback didn’t require much. For example, “the iOS devs don’t want to install a JVM and configure the build.” That was common feedback, and enough to stall the KMP evaluation. Publishing a binary was often the only reasonable way to get started. To prove that KMP “works”. The hope then was to expand KMP once it was in the app.

In 2024, KMP is stable. The Google Android team endorses KMP for shared logic. The bar for “reasonable pushback” is higher. KMP is production ready. It will certainly work when running in an app. That isn’t the question. The question now is if it’ll “work” for your team.

Avoiding KMP because the developers don’t want to install some standard tooling isn’t really “reasonable pushback”. It is what I can a “‘no’ with more words”. The team needs some more convincing. Without it, I would predict more “no’s” in the future.

Tools to use

Linking repos

To simply shared source, there are several options. Some combination of git submodule, git subtree, downloading zip file releases from GitHub, etc. We won’t go deep into it, but if you want to write your own, there are options.

We’ve built the tool “GitPortal” specifically to help KMP teams link a KMP repo into their app repos. There are 2 modes: unidirectional and bidirectional. For the situation we’re discussing here, you use the unidirectional mode, linking an external KMP repo to the app repos. When it’s time so “scale” your KMP adoption, switch to the bidirectional mode, which only requires a change of config.

If you’re curious about what “unidirectional” and “bidirectional” mean, or about a staged approach to evaluating and adopting KMP, I’ll again point to KMP For Native Mobile Teams. It is the core roadmap we use. This post, and several others, explore specific topics in more detail, but that post series ties everything together.

iOS Setup

While setting up a JVM is not a very complex task, there are many options available, and much of the content online is dated. For mac users specifically, setting up build toolchains can be confusing. Similar to Ruby, Python, Node, and others, correctly configuring a JVM involves more research than I’d like. Every time I set up a new machine, I have to dig through posts and threads to figure out current options.

One of the steps discussed in our KMP For Native Mobile Teams doc series relates to documenting the config and setup process, and more importantly, testing and updating those docs. Again, the iOS team is already presented with multiple disadvantages when adopting KMP. Setup is the easiest type to solve. It’s mechanical and easily resolvable. Don’t make things extra hard.

I wrote a post recently that covers the minimal install for iOS developers evaluating KMP. I recommend it, at least as a starting point.

Xcode-Kotlin Plugin

To be able to browse and debug the KMP code, your team will need the tooling configured to do so. We have been publishing the Xcode Kotlin Plugin for several years now. It provides source code formatting for Kotlin, but more importantly, the ability to debug Kotlin directly in Xcode.

The recent 2.0 release adds significant usability and quality-of-life improvements.

To actually edit Kotlin with auto-complete and other features, developers will need to use one of the standard KMP IDE options. The Xcode Kotlin plugin is a “read-mostly” option, but it is great for iOS developers getting started with Kotlin. I also run my KMP iOS builds from Xcode, and the plugin comes in handy for debugging in place.

SKIE

SKIE augments the Swift-facing Kotlin API to bridge modern Kotlin types to their Swift equivalents. For anybody starting a new KMP effort, I’ll say without reservation, turn it on at the start. Your Swift engineers will thank you.

KMP exports an Objective-C API to Swift. This often gets blamed for the loss of Kotlin features when calling Kotlin from Swift. That’s not true. Kotlin and Swift are very different languages. For all of their syntactic similarity, their semantic differences are profound and largely incompatible. Through generated Swift and opinionated choices, SKIE bridges these differences.


¹ Nothing in a production build is “easy”. Publishing binary XCFrameworks, while certainly not “rocket science”, has a lot of moving parts. Many of the things that can go wrong don’t have obvious error messages. While some claim that KMMBridge is complicated, writing the whole thing yourself will be much more complicated. The KMMBridge 1.0 release attempts to address some of the feedback we’ve had, but you can only simplify the process so much.