Since I started Alkaa, a task management open-source app, I have dreamt of also releasing it on iPhone. However, I don’t have experience in developing apps for iOS, and porting all the features to Swift would be an exhaustive job.

Imagine how surprised I was when JetBrains introduced Kotlin Multiplatform and Compose Multiplatform to the public. Suddenly, most of my codebase could be reused and I could keep working with a language and IDE that I am familiar with.

This series will document all the steps, tips, and challenges I faced during this migration. It will focus more on the big picture rather than the details of how to create a Kotlin Multiplatform app. For more information about the basics and how to create your first KMP app, please refer to the official docs.

One key aspect that made the migration easier was the existing application architecture. The Alkaa app follows Hexagonal/Clean architecture concepts, which state that the business rules must not depend on frameworks. By following this rule and applying modularization, we had two benefits:

  • All the business rules work with no additional migration (e.g. when adding a task, then update the home screen widget, and schedule a notification)
  • The modularization allows the migration piece-by-piece instead of committing to port a large portion at once

In the following representation, we can see the separation of concerns and the technology used in each module. For more information about architecture and modularization, please take a look at Google’s official guidelines for app architecture.

Alkaa’s simplified architecture

The modules in green with the Android logo are Android-based and the purple with the Kotlin logo are Kotlin-only. The Kotlin-only modules are easily converted to Kotlin Multiplatform, with minor tweaks in the build.gradle(.kts) and folder replacements.

However, the Android-based modules are more difficult since libraries such as Room, Retrofit, and ViewModel are only available on Android. In order to enable cross-platform support we have two options:

  1. Expose an interface in the common source and implement the native code on each platform
  2. Use a multiplatform alternative for the Android libraries

Since the KMP community has been growing a lot since it reached beta in 2022, we already have a wide range of multiplatform libraries to replace the platform-specific ones.

It’s important to mention that this specific architecture is not required to have a Kotlin Multiplatform application. In the documentation, samples, and several open-source codebases, a lean alternative is used. However, since this series of articles is focused on porting existing production-ready codebases, that solution might not be scalable.

A lean cross-platform architecture

The Domain and Repository modules were quick wins since there was no Android code inside these modules. The first step was understanding how different a Kotlin-only is from a KMP module. Since I had no previous experience, a new module was created to see the structure. In order to add support in Android Studio, the KMP plugin is required.

These are the main differences:

  1. The build.gradle(.kts) has both kotlin("multiplatform") and id("") plugins
  2. Dedicated dependencies for each platform using sourceSets
  3. Dedicated code source directories for each platform inside src/kotlin

After understanding them, it was easy to migrate the existing modules and even create extension functions and Gradle precompiled scripts to speed up the development.

Here’s an example of migrating the repository layer. Most of the changes are moving classes to the new source directory and updating the Gradle file:

Regarding the source location, basically, we have three directories inside a KMP module respective to each platform:commonMain(multiplatform), androidMain (Android), and iosMain (iOS). In the case of the Domain and Repository module, all the code was moved from src/java/main to src/kotlin/commonMain.

Points to consider

Even though the Domain and Repository modules were Kotlin-only, some challenges were faced in the migration:

  • Dependency injection framework — Alkaa uses Koin as a DI framework, which makes the migration easy. Koin already supports multiplatform and the setup for iOS is straightforward. However, keep in mind that if the existing project is using other frameworks such as Dagger or Hilt, more work might be needed.
  • Create an iOS not multiplatform app — when creating the Xcode project, be careful when choosing the project template. Accidentally, I chose the “Multiplatform App” which does not work out of the box with the existing tutorials. After some investigation and choosing “iOS App”, it worked on the first try. For more info about the iOS setup, take a look at the official docs.
Create a new Xcode project screen — Select iOS > App
  • Java-only APIs are not supported by KMP — Alkaa has a simple date and time manipulation that used to rely on java.Calendar. Be aware that Java-based libraries without Kotlin counterparts will not work. For multiplatform compatibility, kotlinx-datetime was used instead.
  • Spaces are not allowed in unit tests — Kotlin introduced test function names with spaces enclosed in backticks (e.g. `test task was inserted`). However, this feature only works with Android min SDK 30, which makes it not feasible for most apps. The solution was replacing the spaces with underlines.
  • “Umbrella” shared module — the initial goal was to have all the modules set as multiplatform and connect them individually in the iOS app. However, during the Xcode setup, we need to provide the path with the generated KMP code. If we have the modules independently, we would need to update the setup every time a new module is added.
    One way of facilitating this setup is by creating an “umbrella” shared module that knows all the other KMP modules. The Xcode setup will rely on a single path and we could add new KMP modules as needed. It also makes the DI injection setup easier.
Simplified view of the migrated architecture with a shared “umbrella” module

The code migrating the Domain and Repository layers can be found on GitHub:

The next article in the series will cover the data sources migration. As mentioned Room doesn’t have cross-platform support, however, Google started a new effort to bring KMM support to some of the Jetpack libraries, which includes DataStore.

In that article, we will see the libraries used, the challenges faced, and how to keep the existing database and user preferences during the migration. In the meantime, all the ported business logic from Alkaa can be found in the following Pull Request:

The following official docs and open-source projects were a great start and helped during the migration:

Source link