Moving forward in the Migrating an Android app to iOS with KMP series, we are now focusing in creating a multiplatform user interface and thanks to Compose Multiplatform, migrating the code from Jetpack Compose is fairly simple, with a few caveats we will cover in this article.
Since there are a lot of details in each of the parts of the migration, I won’t deep dive on all of them. The migration code is shared in the sections below, but if you want to know more about specifics, let me know and we can follow up in an upcoming article.
—
This article is part of a series of migrating an existing Android app to run on iOS using Kotlin Multiplatform. You can access the other articles in the following links:
- Part I: First steps and architecture
- Part II: Data sources and migrations
- Part III: UI and Compose Multiplatform
—
Architecture
To recap, let’s take a look at the application architecture after our last article, where we migrated all the data sources to KMP. We are close to have a full-fledged application running in both Android and iOS, we are only missing the user interface.
Compose multiplatform
The teams in Google and JetBrains are doing a great job in ensuring that the operability between Jetpack and Compose multiplatform works seamlessly. Almost every single Composable in the application simply worked when I migrate to multiplatform. We still need to handle the platform specifics such as notifications, permissions, resources, and soon-to-be-stable libraries such as navigation, ViewModel
and Parcelable
.
For simpler screens, the main changes during the migration were minimum, being mostly how to get resources such as strings and icons. Yes, even the imports stays the same (androidx.compose.*
).
Resources
Speaking about resources, even though we don’t have an embedded way to get resources in the Compose Multiplatform framework yet, we have some great libraries from the community. The one used in Alkaa is the amazing IceRock’s Moko Resources.
After a quick setup, Moko Resources work very similar to the default Android resources, using .xml
files and <string/>
tags. The name convention and localization support is also similar, relying on in different folders with the locale code.
With everything in place, using the resources is as simple as in Jetpack Compose, using the stringResource()
:
// Android | |
val string = MR.strings.category_default_personal.desc().toString(context) | |
// iOS | |
val string = MR.strings.category_default_personal.desc().localized() |
Keep in mind that using resources out of the context of Compose, we would need to provide different implementations per platform since iOS and Android uses different parameters.
// Android | |
val string = MR.strings.category_default_personal.desc().toString(context) | |
// iOS | |
val string = MR.strings.category_default_personal.desc().localized() |
Additional considerations during the setup
- Make sure that the configuration is well set in Xcode during the Build Phase, otherwise it won’t work properly (example in Alkaa)
- In my experience, it was easier to create a single
:resources
module with all the app resources and do all the setup only once - If you are using Kotlin 1.9.0+ with Moko Resources 0.23.0 or below, you might need to apply a workaround (issue page and details)
The code with the initial setup and module migration can be found here.
Native implementations
As expected, some parts of the code need to be implemented using native libraries, while we don’t have multiplatform alternatives. In the case of Alkaa the native implementations are mainly alarms, notifications and home screen widget.
The fun part of Kotlin Multiplatform is that even the Swift/Objective-C native code can be developed using Kotlin. Being an Android/Kotlin developer, this made my life much easier. Also, I need to give a lot of credit to GitHub Copilot that helped on this journey to create Kotlin code for iOS.
Here’s an example of native iOS implementation in Kotlin:
Job Offers
override fun scheduleTaskNotification(task: Task, timeInMillis: Long) { | |
val content = UNMutableNotificationContent() | |
content.setBody(task.title) | |
content.setCategoryIdentifier(CATEGORY_IDENTIFIER_TASK) | |
content.setUserInfo(mapOf(USER_INFO_TASK_ID to task.id)) | |
val nsDate = NSDate.dateWithTimeIntervalSince1970(timeInMillis / 1000.0) | |
val dateComponents = NSCalendar.currentCalendar.components( | |
NSCalendarUnitYear or NSCalendarUnitMonth or NSCalendarUnitDay | |
or NSCalendarUnitHour or NSCalendarUnitMinute, | |
fromDate = nsDate, | |
) | |
val trigger = UNCalendarNotificationTrigger.triggerWithDateMatchingComponents( | |
dateComponents = dateComponents, | |
repeats = false, | |
) | |
val request = UNNotificationRequest.requestWithIdentifier( | |
identifier = task.id.toString(), | |
content = content, | |
trigger = trigger, | |
) | |
NSLog("Scheduling notification for '${task.title}' at '$timeInMillis'") | |
val notificationCenter = UNUserNotificationCenter.currentNotificationCenter() | |
notificationCenter.addNotificationRequest(request) { error -> | |
if (error != null) { | |
NSLog("Error scheduling notification: $error") | |
} | |
} | |
} |
Voyager follows a similar DSL approach to Koin which makes multimodule and encapsulation a breeze. The full migration from Jetpack to Voyager can be found here and here.
ViewModel
The ViewModel
is another one of the Android libraries that are recently ported to Multiplatform, so I ended up using another Moko library called Moko MVVM. It not only adds support to ViewModel
, but also LiveData
, View Binding
and Data Binding
if you are still using it.
package com.escodro.task.presentation.detail.main | |
//... | |
import dev.icerock.moko.mvvm.viewmodel.ViewModel | |
internal class TaskViewModel : ViewModel() { | |
fun loadTaskInfo() { | |
// Everything an Android ViewModel has | |
viewModelScope.launch { doSuspendStuff() } | |
} | |
} |
The migration is straightforward, but platform-specific injections will be needed for Android and iOS.
Dynamic features
One interesting challenged I faced while migrating Alkaa was how to handle the on-demand delivery module. To summarize, in Android, we can set up modules to be downloaded on the fly during app execution to save app size. Even though iOS have a similar feature, I didn’t want to deep dive on it and decided to make it always available for the iOS variant. For my surprise, it was much easier than I thought.
Since KMP allows a different set of dependencies for each platform, it was easy to split them. For Android, we still use our “Split Install” library to download the dynamic module; for iOS we can directly depend on it, making it available right away.
//... | |
androidDependencies { | |
// Module responsible to download from Google Play | |
implementation(projects.libraries.splitInstall) | |
} | |
iosDependencies { | |
// Dynamic Module always available | |
implementation(projects.features.tracker) | |
} | |
//... |
The full implementation can be found here.
Running the apps 🚀
As mentioned in previous articles, having a good modularization and separation of concerns allowed each layer and feature to be ported independently. For most part of the development, Alkaa had a few of features ported and two navigation graphs (Android and KMP) at the same time.
After porting all the features, we have an application that is very similar in the two platforms, with minimum platform-specific code. Keep in mind that Alkaa is a very simple application in features and scope, but gives a good idea on how much we can reuse as it is from the existing Android codebase.
What’s next?
I am very happy that this series is getting outdated due to the amazing job that the Google developers are doing to port more AndroidX libraries to support multiplatform. Here’s a small recap on the libraries with KMP support:
As always, the full code is available in Alkaa’s repository and if you want to dive in the 97 commits that migrate the app from Jetpack to Compose Multiplatform, that’s the pull request:
https://github.com/igorescodro/alkaa/pull/597?source=post_page—–b5e01cc0769a——————————–
In the next article, we are covering how the pipeline was updated to generate both the Android and iOS apps and publish in the store automatically. I will also share some closing thoughts about this amazing journey. If there is something else you want me to cover, please leave a comment and I’ll do my best to address them. 😊
Thank you so much for reading my article! ❤️
This article is previously published on proandroiddev.com