Kotlin Multiplatform at Premise

Nate Ebel
Engineering at Premise
14 min readOct 6, 2022

--

by Nate Ebel, Android Engineer

In January of 2021, we pushed the initial commit for our mobile-shared repo — a new Kotlin Multiplatform (KMP) initiative within the mobile engineering team here at Premise.

Fast forward to 2022, and our usage of Kotlin Multiplatform has increased along with our confidence in the choice to invest in Kotlin Multiplatform as a code sharing solution.

Much has been written comparing Kotlin Multiplatform to other code sharing solutions — debating the merits of one choice or another and often framed within the context of greenfield development or toy sample projects. While these resources are helpful, they’re often lacking a pragmatic grounding in the realities of scaling a production brown-field project.

The discussion is often focused on the core technology rather than on why Kotlin Multiplatform was chosen over other solutions, and how Kotlin Multiplatform was actually then adopted, integrated, and scaled across multiple teams and projects.

This is a part of an ongoing series on our usage of Kotlin Multiplatform at Premise:

I want to focus both on high-level thinking and decision making, and also highlight some of the key technical challenges we had to solve to begin building with Kotlin Multiplatform across both our iOS and Android teams.

We’ll start with how we identified the need for code sharing and why we choose KMP as our solution. And we’ll work our way through the validation, education, and scaling process we’ve gone through over this past year.

This post is the high-level story of our Kotlin Multiplatform journey — the strategy. Throughout this post, we will highlight key technical challenges, and link to other, more focused, posts from the series on the implementation of how we overcame those challenges.

Why code sharing? And why now?

Our code sharing story starts not with Kotlin Multiplatform, and not in comparing KMP to solutions like Flutter or React Native. In fact, it doesn’t start from a strictly technical perspective at all.

It all started because we wanted more accurate and actionable analytics across our mobile platform.

Tip #1 — Let the problem define the solution

Standardizing analytics across iOS and Android

In early 2021, analytics across our mobile team was a bit of a mess.

Both iOS and Android cared about analytics, and tracked events to help understand our system. However, it was a bit like the wild west. Naming was very haphazard. Event types weren’t standardized. And there was significant room for error when implementing any given set of events.

This inconsistency, and uncertainty, hampered our ability to make data-driven decisions.

We knew we needed to change this as we scaled so data-driven decisions could be made for higher-impact with less cost.

To solve this problem, we formed a small team of engineering leaders, to standardize our thinking around analytics for the mobile team. These discussions were quite interesting in their own right, but not central to this story. What was important was the output of those discussions.

Our analytics working group generated several things:

  • clear definitions for unique types of analytics event
  • specific naming conventions for each event type
  • enumerated value sets for key event name components

This thinking was consolidated into a doc to act as a source of truth for developers looking to implement analytics in any new work. We had effectively developed “cross platform thinking” around analytics for our team.

As we reached the end of this process, our strike team had developed two new questions:

  1. Could we codify these rules so they’re strongly enforced in code rather than via a doc?
  2. Could that enforcement be shared across both iOS, Android, and maybe even other teams?

We had finally arrived at the question of code sharing.

Sharing code for shared thinking

Code sharing, particularly for mobile development, is a bit of a “holy grail.” Most would probably agree that some theoretical ideal solution would allow for 100% code reuse across all mobile platforms — while still offering peak performance and beautiful platform-native experiences.

Most that have walked this path would probably also agree that this fantasy solution rarely works out in practice; and certainly not without tradeoffs.

For our use case though, we didn’t need, or want, a 100% code sharing solution. We wanted something much more focused. Honestly, simply sharing event name constants would have been a viable v1 solution and was discussed as such.

So, our decision making backdrop at this point was

  • We have a clear set of rules and definitions for analytics events.
  • We want to share this logic across iOS, Android (and maybe beyond).
  • We don’t need shared UI.
  • We need to share code using a tech stack that at least a significant portion of our team is familiar with.

Tip #2 — When evaluating a code sharing solution, consider both the technical, and human factors of each option.

Choosing Kotlin Multiplatform

We chose Kotlin Multiplatform as our code sharing solution for serval reasons.

  • KMP focuses on business logic — not UI.
  • KMP is relatively easy to integrate into a brown-field project.
  • We had a great deal of Kotlin knowledge on our Android team.
  • We had interest in KMP on our Android team.
  • We had equal interest in KMP from our iOS team.

Kotlin Multiplatform was an obvious choice for us. Both our iOS, and Android tech leads arrived at this thought on their own before even having the full discussion.

It fit our use case of business logic sharing.

It was considered relatively low risk. If, for whatever reason, the KMP code didn’t work, the Android team could easily move the Kotlin code into our repo, and the iOS could translate it into Swift and bring it into their project.

At this point, we were comfortable enough to invest in a proof of concept validation.

Validating Kotlin Multiplatform for brown-field projects

Before we could realistically consider long-term investment into Kotlin Multiplatform, we needed to identify key challenges and risks.

We needed to understand how to solve the challenges and mitigate the risks before we felt comfortable adopting the proposed solution.

Some of these primary questions included:

  • Where to store this new Kotlin Multiplatform project?
  • What multiplatform targets to choose?
  • How to structure a Kotlin Multiplatform project?
  • How to provide a productive local development experience?
  • How to ensure low-cost deployment via CI?
  • How to integrate the KMP code into both the existing iOS and Android projects?
  • How to onboard other developers?

Structuring our project

Many Kotlin Multiplatform examples focus on the idea of building an app from scratch. In this use case, it’s common to structure your KMP project as a monorepo with the Shared, Android, and iOS projects all living side-by-side.

However, we already had existing iOS and Android projects in their own repos. So this typical project structure wouldn’t work for us.

We realized we had to shift our thinking a bit.

Rather than thinking of it as building a Kotlin Multiplatform app, we found it more helpful to think of the project as building a separate Kotlin Multiplatform library to then be consumed by each of the existing projects mobile projects.

With that in mind, we created a new Kotlin Multiplatform project named mobile-shared with a single :analytics module to start.

This structure would allow us to consolidate all the shared code into this specific repo, which would then need to be built, published, and consumed by our iOS and Android projects.

Tip #3 — For brownfield projects, think of building a multiplatform library rather than a new multiplatform app

Choosing which platforms to support

We needed our shared code to work for both our iOS and Android apps. Our first thought was that we could simply choose iOS and Android targets.

However, that’s not where we landed.

We envisioned a world in which our multiplatform experiment went well and in which we might want to share our business event logic with other projects. Many of our services, including our primary mobile-proxy service, are JVM-based — being a mix of Scala, Java, and Kotlin. We thought it would be interesting to see if we could generate an artifact that could be consumed by those projects as well.

So, rather than using an Android target for our proof-of-concept, we chose a JVM target. This allows us to still consume the JVM artifact in our Android app, but gives us the option to potentially reuse the code in other services down the line.

We were able to make this choice, because our shared analytics strategy was platform agnostic. We weren’t planning to rely on any Android-specific apis, or SDKs for what we were planning to build.

Local development

Our mobile team very much cares about developer productivity, and the overall developer experience of our projects. Because of this, it was vital for us to understand how developers could efficiently build, test, and consume the KMP project locally.

We envisioned developers working on a new feature, opening up the mobile-shared project, building and testing their code locally, before then eventually pushing back to GitHub and creating a PR.

These seem like relatively straightforward tasks on the surface, but they raised a handful of interesting questions for us.

Tip #4 — Invest in the developer experience for any code sharing solution

How do developers build the Kotlin Multiplatform locally?

Kotlin Multiplatform relies on the Gradle build tool, and by extension, the Java Virtual Machine (JVM). The Android target requires Android build tooling, and the iOS target requires XCode build tooling.

This means that whether a dev was on the iOS or Android team, they likely had some tooling to install and configure before the KMP project could be built.

For iOS devs, they had to:

  • Install the Android SDK.
  • Install a JDK.

For Android devs they had to:

  • Install XCode build tools.

Once the tooling was installed and configured properly, we could run our KMP project’s build task using the gradle-wrapper to assemble and test the project.

This left us with two primary artifacts:

  • A .jar file for the JVM target
  • A XCFramework for the iOS target

Now that we had these output artifacts, the next step was to understand how to actually consume those from within the iOS and Android projects.

How do developers integrate the Kotlin Multiplatform artifacts locally?

The fastest way to validate that our XCFramework and .jar could be integrated was to simply move those files into the iOS and Android project directories, and point the projects at them.

Both XCode and Gradle make it pretty easy to consume a framework or jar as a dependency.

This let us validate that our sample shared code could, in fact, be consumed from both iOS and Android using a single build process.

The next step was a move towards scalability.

Manually copying files around is not a great everyday development experience.

So, for the Android team, we explored a local deployment flow in which we could build our .jar, publish to our local Maven repository, and consume in our Android project like we would any other external dependency.

We applied the Maven Publish Plugin to our :analytics module to gain access to the publish and publishToMavenLocal tasks.

  • The publishtask generates metadata and associates it with a build artifact for publishing to some maven repository.
  • The publishToMavenLocal task deploys the maven publication (artifact + metadata) to the local Maven repository.

Once the JVM output artifact was published to our local Maven repository, we could access that maven coordinate from the mavenLocal() repository and integrate the dependency into our project as if it were coming from mavenCentral() or an internal Maven repository.

This flow is really powerful for the team as it allows for efficient local dev/deploy/test feedback cycles without having to publish anything to our internal Maven repository.

CI / CD

Building and deploying as part of our CI/CD pipeline faced similar challenges to the local build/deploy workflow.

Building a Kotlin Multiplatform project using GitHub Actions

Our mobile teams use GitHub Actions for our CI builds. We use the GitHub-hosted runners, so we needed to make sure that whatever runner we chose included the necessary build dependencies on the VM.

This was actually a pretty easy decision though. Because we need the XCode build tools to build the iOS target of our KMP project, we need to use the MacOS runner.

This was generally fine. The list of pre-installed dependencies is quite long, and included everything we needed. The only real concern was the cost. The macOS runner is more expensive than the ubuntu-runner our Android team typically uses.

After running our first builds on that macOS runner though, it was determined that it was not prohibitively expensive since the builds were fast on such a small project.

This build step included building both the JVM and iOS artifacts when running the build Gradle task.

Monitoring build performance using Gradle Enterprise

From that early point in the project, we also integrated Gradle Enterprise so we could collect buildscans and monitor the build performance over time. This was done to keep the KMP builds as fast as possible and lessen the overall cost of using the macOS runner.

Deploying Kotlin Multiplatform artifacts using GitHub Actions

Deploying of the JVM artifact was quite simple. We wanted to deploy the .jar as a maven artifact within our internal Maven server. To do this, we used the Gradle Artifactory Plugin. This plugin works in conjunction with the Maven Publish Plugin to deploy any generated Maven publications to some external Maven server.

Using the Artifactory plugin, we configured:

  • the url of our internal Artifactory server
  • the publication credentials
  • which publications to include (jvm, kotlinMultiplatform)

Once the plugin was applied to our :analytics module and configured, we could use the artifactoryPublishand artifactoryDeployGradle tasks to publish our .jar file to our internal Artifactory server. Once that was done, we could access the desired dependency from the internal server when building our Android project.

Deploying the iOS artifact was more challenging and much more interesting. Our goal, was to consume the iOS code as a Swift Package.

To support the deployment, some changes were made to the project outside of any CI flow:

  • We setup an additional GitHub repo to store a Swift Package and associated metadata.
  • We integrated the Multiplatform Swift Package plugin which provides a Gradle task that packages a XCFramework as a Swift Package

Once those changes were in place, the general CI flow for publishing the Swift Package was:

  • build the JVM and iOS targets
  • generate the Swift Package from the XCFramework
  • publish the Swift Package, and metadata, to the external repo by pushing a commit

Once the Swift Package is committed to the mobile-shared-swift-package repo, the XCode build can be pointed to that repo, with proper credentials, and can consume that package during the iOS CI build.

Once we reached this point, we had successfully validated all our questions and concerns. We had a working, end-to-end solution that made us feel comfortable continuing on with our investment into Kotlin Multiplatform. The next step was to present the findings to our team, and to ramp up in developer education.

Developer education

Once we had our working proof-of-concept, we wanted to share the findings with the team, and discuss our proposal for moving into the next phase of the project.

For this, we did several things:

  • We prepared documentation on how to setup and build the project.
  • Multiple calls were scheduled with the team to walk through the technical aspects of the proposed solution.
  • A doc was prepared for review; outlining how our Analytics naming rules would be codified and enforced using Kotlin Multiplatform.
  • We hosted several working sessions with developers to help setup the project, and get ramped up on how to consume the multiplatform code.
  • Several working sessions were dedicated to rapid prototyping of our v1 release.

Through these efforts, we moved towards a “live beta” phase of the project where we then continued to invest and expand on our usage of Kotlin Multiplatform.

Testing Kotlin Multiplatform in production

Even in early 2021, Kotlin Multiplatform was quite stable for use cases not requiring multithreading in native build targets such as iOS.

For our use case, we were generally only defining sealed classes, enums, constants, and a few interfaces to wire everything together. So, we viewed our shipping of KMP in production as a “low risk” endeavor.

That said, we still took a very measured approach to shipping Kotlin Multiplatform code into our iOS and Android apps — just in case something did go wrong.

iOS shipped Kotlin Multiplatform first. They added a single event that shadowed an existing event from the previous analytics implementation. This event was only fired in one single place, making it a low-cost investment. After several weeks in production, no issues were found, and it was deemed safe to continue adding more events from the shared code.

On Android, the experience was much the same. A single, commonly sent, event was added using shared code, and after weeks of observation, the endeavor was deemed safe for production.

Revisiting our Kotlin Multiplatform investment one year later

The process outlined in this post took several months from first conception to shipping our first Kotlin Multiplatform in production.

After a year, we’re still pretty bullish on Kotlin Multiplatform — at least for non user-facing use cases.

Our investment into the shared analytics use case has been very much a success. The ability to strongly enforce even naming consistency across both iOS and Android has been a win on its own. Being able to share the event definitions when teams work on projects at different times is another big win. Through developing our shared apis, we also took the opportunity to improve our thinking around event properties as well. In particular, we marked all events with a schema property making event schemas easier to identify in Amplitude.

There have been challenges as well. In particular, the iOS developer experience is not as polished as it is for the JVM and Android dev experience.

The two main challenges our iOS team have faced have been:

  1. Kotlin Multiplatform generates Objective-C code rather than Swift. So, some of the apis are difficult to work with from Swift when being consumed via Swift Package based on an XCFramework.
  2. Syncing XCFrameworks (as Swift Packages) became slower and slower over time. This was due to committing the built XCFramework directly to our publication repo which causes Xcode’s deep clone of the repo to fetch a multi-gigabyte repo on fresh syncs.

The issue with consuming KMP code from Swift is a real one. Our iOS team has started to build some small wrapper layers around the KMP code to make it easier to work with in Swift. This largely revolves around mapping Objective-C types into the desired Swift types as outlined in the KMP Objective-C/Kotlin interop guide. There are additional tools and features coming online to help with this problem, so hopefully this part of the developer experience continues to evolve for the better.

The issue with slow Swift Package syncs is something we’re actively working on fixing at the moment. The solution in a nutshell is to move away from a binary distribution approach for the Swift Package to a remote url distribution approach. This allows us to store the XCFramework as a zip file outside of any git repository. In doin this, it means Xcode only ever has to sync a single version of the XCFramework so syncs stay fast over time.

What’s next?

Thanks to the success of the analytics use case, we’ve turned to Kotlin Multiplatform for new use cases including:

  • Deeplinking
  • Performance monitoring
  • Recommendations
  • Networking

There has been some experimentation into using Kotlin Multiplatform, in conjunction with Compose Multiplatform, to build some internal tools, but nothing significant has come from those efforts yet.

Will we ever embrace KMP to the level of shared presenters or even shared UI? I doubt it. But, we’re excited to see where the technology, and developer community are going.

I think we’ll continue to invest heavily into our analytics and deeplinking use cases, and think we’ll likely add new features into shared code as they come up.

For more on how we’ve been using Kotlin Multiplatform at Premise, check out Part 2 in our series:

Kotlin Multiplatform Project Structure for Integrating with Brownfield Applications

Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our Careers page.

--

--