Unifying our workflow: the journey to monorepo with Kotlin Multiplatform Mobile

Marco Righini
XapoLabs
Published in
4 min readJun 26, 2023
Photo by Possessed Photography on Unsplash

Over the years since embarking on our Kotlin Multiplatform (KMM) journey at Xapo Bank, we’ve taken strides from its initial alpha stage to employing it as a key tool for sharing code between Android and iOS, thereby reducing inconsistency issues.

This article walks you through the evolutionary phases that culminated in the monorepo structure we use today.

This is not a silver bullet but a natural progression developed through trial and error, as we tackled challenges inherent to KMM adoption. Furthermore, we are constantly refining this architecture to make it even better.

The Initiation: 3 separate repositories

Our preliminary approach was to test the waters of KMM feasibility and stability.
We achieved this by creating a separate KMM repository to house the shared code while leaving the structure of the mobile projects unaltered.

The repositories were organized as follows:

  1. KMM repository for the shared code. Upon every merge on the main integration branch, a Swift package was built and published on GitHub.
  2. Android repository incorporated the KMM repository as a git submodule, building through Gradle composite build.
  3. iOS repository used the prebuilt KMM Swift Package from GitHub.
3 repositories: Android, iOS and KMM

We employed a strategy akin to the one detailed in this article for publishing a Swift Package on GitHub and consuming it via Swift Package Manager.

However, we quickly discovered that

This approach was still requiring a considerable lead time for changes, with 3 PRs for each multiplatform adjustment. Moreover, the KMM PR needed to be merged before triggering the final round of CI jobs related to the iOS PR.

It was clear that we needed to consolidate our repositories.

The Transition: consolidating to 2 repositories

In an effort to maximize return on investment, we decided to merge the KMM repository into the Android one, thereby reducing the number of repositories to two.

The repository structure evolved into:

  • Android+KMM repository: starting from the Android project, we transitioned the KMM submodule into a straightforward folder. This eliminated the need for Gradle composite build. A Swift package is still built and published on GitHub after every merge on the main integration branch.
  • iOS repository which utilized the prebuilt KMM Swift Package from GitHub.
2 repositories: Android+KMM repository and iOS repository

This change lifted developers’ spirits. Reducing one PR meant a noticeable boost in productivity.

Although one last step was missing

The requirement of 2 separate PRs still prevented the CI jobs from running in parallel, resulting in suboptimal lead time for changes.

The Resolution: embracing the monorepo structure

In our final iteration, we are testing a monorepo structure, segregating the different platforms and KMM code into distinct folders:

  • kmm folder for the shared code.
    An XCFramework is constructed locally every time an iOS build is initiated from XCode. A static Package.swift file references the XCFramework path and defines the local KMM Swift Package.
  • android folder where the Android gradle modules incorporate the KMM modules defined in the KMM folder.
  • ios folder housing the iOS source code and XCode configuration.
    The KMM Swift package is integrated as a local package with a relative path to the Package.swift.
Monorepo

One highlight of this structure is the local inclusion and refreshing of the KMM Swift Package during an iOS build.

Local KMM Swift Package included in the iOS build

This is achieved by incorporating a Build -> Pre-Actions entry for the target schema in XCode.
A crucial detail for this pre-action is to run
xcodebuild -resolvePackageDependencies after building the XCFramework, which seems to help alleviate autocomplete issues in XCode.

Conclusion

This article serves to encapsulate our evolutionary journey with KMM, starting from the use of multiple repositories to adopting a streamlined monorepo structure, avoiding the publication of artifacts.

Our approach is open for further optimization opportunities, such as building for a specific target or integrating a remote cache (bear in mind that, due to this issue, the linking phase of the XCFramework task still remains not cacheable).

We’ve purposely kept this article concise to provide a clear, digestible overview of our journey. Based on the feedback we anticipate from you, we plan to produce more in-depth content in the future.

Please comment below or reach out to me on Twitter or LinkedIn.

Thank you very much for spending your time reading the article. Don’t forget to 👏 if you liked it.

If you’re interested in learning more about Xapo Bank — Bitcoin Bank providing simple and safe usability to retail customers, explore the resources at xapobank.com.

Thanks to Manuel Rebollo and Pepe Guerrero for proofreading the article.

--

--