Recently I reactivated my old project Timi which I used to learn Compose. This time my focus is on learning the iOS side of Kotlin Multiplatform, I'm hoping that this experience will help me better understand my colleagues on the other platform.
Working on a smaller project also allows for more experiments, in this project I'll try to share as much code as possible, and see how that integrates on both mobile platforms.
In this article, I'll share my experience making a Kotlin Multiplatform app from an Android only codebase. If you're interested in the commits of this process, they're available in the GitHub repository (starting from the 2022 commits).
The modularization used for this project is explained more in-depth in my Modularizing a Kotlin Multiplatform Mobile Project.
Adding Kotlin Multiplatform and iOS to the codebase
- My first step was creating a new playground project based on the KaMPKit starter. I removed everything I didn't need and added a simple class resembling one feature of Timi (A stopwatch). I used this class on both Android and iOS to see if everything works.
- In the Timi codebase, I did some gradle related clean up to make Kotlin Multiplatform integration easier (replaced Groovy with Kotlin DSL and added refreshVersions for dependency version management)
- The next step was copying over the playground KMP module and the iOS project into the Timi codebase.
- The first KMP integration was done o Android, and then I got it working on iOS.
After these steps, I had a working bare-bones Kotlin Multiplatform app which shares some code with both platforms.
Moving existing logic to Kotlin Multiplatform
Keep in mind that the project is pretty small, which allowed me to do the refactor in much bigger steps without fear of breaking something.
The Android codebase was modularized, which allowed me to move the logic to KMP on a module by module basis.
The core module
At the start, I focused on the "core" module of the application. This module contains interfaces and data structures which are used in many of the other modules. Fortunately, the module didn't contain any Java specific libraries, so it was easy to move it to KMP.
The problem came with Dependency Injection, the Android app used Dagger Hilt for DI, but for Kotlin Multiplatform I wanted to use Koin. In order to make the process straightforward without a lot of changes, I marked the Hilt module as a KoinComponent and used Koin to retrieve the dependencies:
@Module
@InstallIn(SingletonComponent::class)
object TimestampModule: KoinComponent {
@Provides
internal fun provideTimestampProvider(): TimestampProvider = get()
}
This allowed me to use Hilt in the Android app, but still retrieve the KMP dependencies from Koin.
After the core module I focused on moving the feature modules which contained all the layers (Presentation, Domain, Data and UI which I didn't move).
Domain and Data layer
At first, I stared with the Domain and Data layer of the stopwatch module. This module seemed like a good start because it had the least amount of external dependencies.
Moving the stopwatch classes was pretty easy just like the core module, however there were places where Java libraries were used, so I had to replace them with a Kotlin alternative, or just "improvise" and leave a TODO comment for later 🙊
I moved the Unit tests last because I wanted to ensure that the production code was still working after moving. After everything was green (The tests passed) I had to also move them to KMP. This task was a lot more involved because I wrote these tests using JUnit5 which is not available in Common Kotlin Multiplatform. To move the tests I had to refactor them to use plain framework features (no nested classes, no parameterized tests) and then move them to KMP.
More in-depth explanation about the Kotlin Multiplatform testing can be found here:
Presentation layer
After checking that the Android app stopwatch still works, I focused on the Presentation layer of the stopwatch module. This included moving the ViewModel to the shared code (I still haven't come up with a mechanism for saving the data on process death).
After moving the presentation layer successfully, Â I created some basic UI for the iOS app. Once both the apps had the stopwatch feature working, I moved on to the next feature module. Every module was moved repeating these steps: moving Data and Domain, then Presentation, followed by iOS UI.
Problems
While moving logic into KMP, I've encountered some problems which I either fixed, or left for my future self to fix.
Java dependencies
When it comes to the Java standard library, Kotlin should have corresponding functionalities, although there are some exceptions (mostly related to concurrency). The official Kotlin documentation provides great information about if a given functionality is available on Kotlin/JVM as well as Kotlin Native.
Libraries might be harder to migrate, even though the Kotlin Multiplatform ecosystem is growing every day, it is still relatively young. Because of this, not every library supports KMP or has an alternative to it. Fortunately all the basic blocks of mobile development exist: Networking (Ktor or Apollo), Database (SQLDelight), Key Value Store (Settings), Dependency Injection (Koin) the list could go on and on. To help with this, there exists a curated list of Multiplatform libraries.
Compose and Swift UI color
Timi heavily focuses on colors, every task is assigned a color. This color will be then used in other screens of the app. Unfortunately, the Android app did not use a framework-agnostic color abstraction and just used the Compose Color class everywhere. As you can guess, this was problematic when moving classes from Android to KMP.
The solution that I came up with is this:
data class TaskColor(
val red: Float,
val green: Float,
val blue: Float,
)
Which can be used on both the Compose and SwiftUI framework.
The database
Fortunately, the Android app used SQLDelight for the database, which has support for Kotlin Multiplatform. The transition to KMP only required changing the database path, package and the database driver. I didn't create any migration files because the database is still in its early phases and will probable change in the near future. Although, I'm pretty sure it should work with no problems as long as the database name and structure isn't altered.
The database also had integration tests which used the in-memory database driver, unfortunately this driver only exists on the JVM. For Kotlin / Native, there is an in-memory database option, but it requires some tweaking in order to work (Although this is a topic for a separate article 😄).
What's next
I'll try to move as much as I can into the shared codebase, and create a corresponding iOS screen for that feature. When it comes to the ViewModel, I'd like to come up with a way to survive process death on Android. In the meantime, I'll try to understand the iOS platform better and find out the best ways to share / consume the shared code on iOS.
Thank you for reading this, I hope my journey might be helpful for some of you wanting to try out Kotlin Multiplatform on a more "brown field" project. My migration from Android to Kotlin Multiplatform isn't as clean as I wanted it to be. But this mostly has to do with the fact that I wanted to get my hands dirty with SwfitUi as soon as possible.