Upgrade to Pro — share decks privately, control downloads, hide ads and more …

I didn’t know my Android native app could run o...

Avatar for Fabio Catinella Fabio Catinella
November 02, 2025
3

I didn’t know my Android native app could run on iOS - Compose Multiplatform in action!

What if you, as an Android developer, could suddenly develop for iOS without learning a new language or framework? This isn't a fantasy; it's the reality made possible by Compose Multiplatform.



Even libraries designed exclusively for Android, such as androidx.lifecycle, now work on iOS. Often right out of the box!


During this talk, we will explore the basics of Kotlin Multiplatform (KMP) and Compose Multiplatform (CMP). Then, we will take a real native Android application, built entirely with Kotlin and Jetpack Compose, and methodically migrate it to run on iOS, changing only a few lines of code.

Avatar for Fabio Catinella

Fabio Catinella

November 02, 2025
Tweet

Transcript

  1. Fabio Catinella Senior Android Developer @ I didn’t know my

    Android native app could run on iOS Compose Multipla tf orm in action!
  2. Agenda • Kotlin beyond Android • What is a CMP?

    • CMP App Requirements • Let’s migrate our app • Conclusions
  3. Disclaimer I’m not an iOS Developer so, I might say

    something incorrect about it. Please take my words with a grain of salt.
  4. Kotlin beyond Android Source: h tt ps://kotlinlang.org/ When speaking about

    Kotlin, people usually think to Mobile development, in pa rt icular to Android Development.
  5. Kotlin beyond Android Source: h tt ps://kotlinlang.org/ But Kotlin doesn’t

    limit to Android. It can be used to develop many other products like, websites or backends. It can also used to call native like it is usually done with C or C++
  6. Kotlin beyond Android Source: h tt ps://www.jetbrains.com/kotlin-multipla tf orm/ Thanks

    to its interoperability, though. Kotlin made a step fu rt her with the technology called Kotlin Multi Pla tf orm (KMP) that allows code sharing between di ff erent pla tf orms.
  7. Kotlin beyond Android What makes KMP so special it is

    the freedom it leaves to developers to decide how much code to shared maintaining it native. Source: h tt ps://www.jetbrains.com/kotlin-multipla tf orm/ UI Logic
  8. Kotlin beyond Android You can share an isolated and critical

    pa rt of the app. Source: h tt ps://www.jetbrains.com/kotlin-multipla tf orm/ UI Logic
  9. Kotlin beyond Android Or you can share the entire Business

    logic of your app while keeping the UI Native. Source: h tt ps://www.jetbrains.com/kotlin-multipla tf orm/ UI Logic
  10. Kotlin beyond Android Source: h tt ps://developer.android.com/kotlin/multipla tf orm A

    lot of Jetpack libraries have already been migrated to take advantage of KMP. • Lifecycle • Datastore • Room • SavedState • SQLite • And many others…
  11. Compose Multiplatform on Android On Android, CMP relies on Jetpack

    Compose, so it is suppo rt ed out of the box without (almost) any changes by the developer.
  12. Compose Multiplatform on iOS On iOS instead, Compose Multipla tf

    orm uses a canvas-based rendering without compromising native feeling.
  13. Compose Multiplatform on iOS • Scrolling behavior that matches native

    iOS physics. • Text editing with native selection and RTL suppo rt • Drag-and-drop integration with the system • Adaptive UI that respects system se tt ing like font size and contrast • Navigation gestures that feel natural to iOS users. Source: h tt ps://blog.jetbrains.com/kotlin/2025/05/compose-multipla tf orm-1-8-0-released-compose-multipla tf orm-for-ios-is-stable-and-production-ready/
  14. Compose Multiplatform on iOS Source: h tt ps://blog.jetbrains.com/kotlin/2025/05/compose-multipla tf orm-1-8-0-released-compose-multipla

    tf orm-for-ios-is-stable-and-production-ready/ Compose Multipla tf orm is designed for Host Interoperability: It can be embedded as a single View (UIView/Swi ft UI) within a pre-existing native application.
  15. Liquid Glass Source: h tt ps://medium.com/mateedevs/liquid-glass-components-in-compose-multipla tf orm-71b7a9 ff c56d

    Due to the way the components in liquid glass are measured by CMP. They are not quite usable at the moment.
  16. Compose Multiplatform App Ingredients • Using Compose for the UI

    • App fully wri tt en in Kotlin (libraries included) ✅
  17. Kotlin only libraries Dagger / Hilt If you are an

    Android developer, it is very likely that you use these libraries: Retro fi t
  18. Compose Multiplatform App Ingredients • Using Compose for the UI

    • App fully wri tt en in Kotlin (libraries included) ✅ ✅
  19. Start migration To begin migrating a native application, the fi

    rst step is to create a new Compose Multipla tf orm (CMP) project, which can be easily and e ff ectively done using the KMP wizard. Source: h tt ps://kmp.jetbrains.com/
  20. Start migration Thanks to the new KMP Plugin included in

    Android Studio (2025.1.1) Narwal, it is possible to create a KMP app directly also from the integrated wizard. Source: h tt ps://blog.jetbrains.com/kotlin/2025/05/kotlin-multipla tf orm-tooling-now-in-intellij-idea-and-android-studio/
  21. Source Set “A Kotlin source set is a set of

    source fi les with its own targets, dependencies, and compiler options. It's the main way to share code in multipla tf orm projects.” Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/multipla tf orm-discover-project.html
  22. Source Sets Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/multipla tf orm-discover-project.html#source-sets

    iOS iosMain iOS can run on devices with di ff erent CPU architectures. For example: iPhones and iPads use a Arm64 CPUs, while old Macs* still use X86(_64) CPUs. *Macs with Apple Silicon use the same Arm64 architecture as iPhones and iPads, so it could be possible avoiding to have the iosX86 target.
  23. Source Sets Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/multipla tf orm-discover-project.html#source-sets

    iosArm64 iosX86 iosMain iOS can run on devices with di ff erent CPU architectures. For example: iPhones and iPads use a Arm64 CPUs, while old Macs* still use X86(_64) CPUs. *Macs with Apple Silicon use the same Arm64 architecture as iPhones and iPads, so it could be possible avoiding to have the iosX86 target.
  24. Intermediate Source Sets Intermediate source sets act as a parent

    for a group of pla tf orm-speci fi c source sets. For example, if you're targeting Android, iOS, and desktop, you might want to share code between just the iOS targets ( iosArm64 and iosX86). Instead of duplicating this code, you create an intermediate source set, typically named iosMain. Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/multipla tf orm-discover-project.html#source-sets iosArm64 iosX86 iosMain
  25. CommonMain CommonMain is a source set that contains all the

    code shared across all the di ff erent pla tf orms you're targeting. This can include core business logic, data models, and any other code that doesn't depend on a speci fi c pla tf orm's APIs Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/multipla tf orm-discover-project.html#source-sets iosArm64 iosX86 Android
  26. Migration goal To successfully migrate our app, the goal would

    be to move as much code as possible from the old Android project to the new source set common main Android Project commonMain
  27. Why this error? OKh tt p exists only on one

    of the two targets of our project. (Android)
  28. Actual e Expected function Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/multipla

    tf orm-expect-actual.html “Expected and actual function allow you to access pla tf orm-speci fi c APIs from Kotlin Multipla tf orm modules. You can provide pla tf orm-agnostic APIs in the common code.” 
 expect fun getHttpClientEngine(): HttpClientEngine
  29. Actual e Expected function Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/multipla

    tf orm-expect-actual.html “Expected and actual function allow you to access pla tf orm-speci fi c APIs from Kotlin Multipla tf orm modules. You can provide pla tf orm-agnostic APIs in the common code.” 
 actual fun getHttpClientEngine() = OkHttp.create()
  30. Dependencies and source sets Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/multipla

    tf orm-add-dependencies.html Every source set can have its own exclusive dependencies. sourceSets { iosMain.dependencies { ... } androidMain.dependencies { ... } commonMain.dependencies { ... } commonTest.dependencies { ... } }
  31. DarwinClient Apple devices use Darwin as H tt pClient. Ktor

    o ff ers a Kotlin/Native client engine for them. To use it, we just need to add its dependency in the right source set sourceSets { iosMain.dependencies { implementation(libs.ktor.client.darwin) } ... }
  32. fun buildSimpleClient(): HttpClient { return HttpClient(getHttpClientEngine()).config { install(ContentNegotiation) { json(

    Json { isLenient = true ignoreUnknownKeys = true } ) } install(RateLimitPlugin) } } 
 expect fun getHttpClientEngine(): HttpClientEngine
  33. Second error -> TokenManager TokenManager implementation is dependent on LocalStorage.

    interface TokenManager { fun getAccessToken(): String? fun saveAccessToken(value: String) } class TokenManagerImpl( private val localStorage: LocalStorage ) : TokenManager { override fun getAccessToken(): String? = localStorage.getString(ACCESS_TOKEN_KEY) override fun saveAccessToken(value: String) = localStorage.putString(key = ACCESS_TOKEN_KEY, value = value) }
  34. Second error -> TokenManager Diving in inside the LocalStorage implementation

    we found out it is dependent on SharedPreferences which is an Android-only concept. class LocalStorageImpl( context: Context, private val cipher: SimpleCipher, ) : LocalStorage { ... private val sharedPreferences = context.getSharedPreferences(LOCAL_STORAGE, Context.MODE_PRIVATE) ... }
  35. KMP gives us access to Native API in the iOS

    Module This is how we can implement an iOS speci fi c LocalStorage implementation import platform.Foundation.NSUserDefaults class iOSLocalStorage() : LocalStorage { override fun getString(key: String): String? = NSUserDefaults.standardUserDefaults.stringForKey(key) override fun putString(key: String, value: String) = NSUserDefaults.standardUserDefaults.setObject(value = value, forKey = key) override fun delete(key: String) = NSUserDefaults.standardUserDefaults.removeObjectForKey(key) }
  36. Resources in CMP Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/compose-multipla tf

    orm-resources.html Compose Multipla tf orm provides a special compose-multiplatform- resources library and Gradle plugin suppo rt for accessing resources in common code across all suppo rt ed pla tf orms.
  37. Resources in CMP Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/compose-multipla tf

    orm-resources.html sourceSets { ... commonMain.dependencies { implementation(compose.components.resources) } ... }
  38. Resources in CMP Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/compose-multipla tf

    orm-resources.html Create a new directory composeResources in the source set directory you want to add the resources to and use it like you would on Android.
  39. Resources in CMP Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/compose-multipla tf

    orm-resources.html In Android, resources are identi fi ed by the generated R class, and each resource corresponds to an integer ID. In CMP, resources have a similar but slightly di ff erent approach. The Res class is used, but instead of indexing an integer, it indexes a Resources object.
  40. Components @Composable fun GameCover( game: Game, modifier: Modifier = Modifier,

    contentScale: ContentScale = ContentScale.FillHeight ) { GameDbImage( model = game.coverUrl ?: "", contentScale = contentScale, modifier = modifier .clip( RoundedCornerShape(4.dp) ), isCrossFadeEnable = false, previewResourceId = R.drawable.preview_cover_image ) } (Jetpack Compose)
  41. Components @Composable fun GameCover( game: Game, modifier: Modifier = Modifier,

    contentScale: ContentScale = ContentScale.FillHeight ) { GameDbImage( model = game.coverUrl ?: "", contentScale = contentScale, modifier = modifier .clip( RoundedCornerShape(4.dp) ), isCrossFadeEnable = false, previewResourceId = Res.drawable.preview_cover_image ) } (Compose Multiplatform)
  42. Navigation To handle navigation in my native app I chose

    an handy library called Compose Destination by Rafael Costa which relies a lot on KSP. Unfo rt unately (although it may be in the future) it’s not KMP ready. I had to go back to standard navigation using the Jetpack Compose Navigation library
  43. Navigation org.jetbrains.androidx.navigation:navigation-compose NavHost(navController = navController, startDestination = Routes.Home) { composable<Routes.Home>

    { HomeRoute( navigator = navController ) } composable<Routes.Detail>( deepLinks = listOf( navDeepLink { uriPattern = "app://gamedb.fabiocati.it/{gameId}" }, ) ) { val route: Routes.Detail = it.toRoute() GameDetailsRoute( gameId = route.gameId, navigator = navController ) } }
  44. Navigation org.jetbrains.androidx.navigation:navigation-compose NavHost(navController = navController, startDestination = Routes.Home) { composable<Routes.Home>

    { HomeRoute( navigator = navController ) } composable<Routes.Detail>( deepLinks = listOf( navDeepLink { uriPattern = "app://gamedb.fabiocati.it/{gameId}" }, ) ) { val route: Routes.Detail = it.toRoute() GameDetailsRoute( gameId = route.gameId, navigator = navController ) } }
  45. Navigation org.jetbrains.androidx.navigation:navigation-compose NavHost(navController = navController, startDestination = Routes.Home) { composable<Routes.Home>

    { HomeRoute( navigator = navController ) } composable<Routes.Detail>( deepLinks = listOf( navDeepLink<Routes.Detail>( basePath = "app://gamedb.fabiocati.it" ) ) ) { val route: Routes.Detail = it.toRoute() GameDetailsRoute( gameId = route.gameId, navigator = navController ) } }
  46. Deeplinking On Android, the deep link URIs sent to the

    app are available as a pa rt of the Intent that triggered the deep link. A cross-pla tf orm implementation needs a universal way to listen for deep links. This can be achieved by using a Singleton declared in the commonMain source set. When the ComposeApp sta rt s it will check the data saved inside that singleton and will navigate to the right page. Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/compose-navigation-deep-links.html
  47. Deeplinking object ExternalUriHandler { private var cached: String? = null

    var listener: ((uri: String) -> Unit)? = null set(value) { field = value if (value != null) { cached?.let { value.invoke(it) } cached = null } } fun onNewUri(uri: String) { cached = uri listener?.let { it.invoke(uri) cached = null } } } Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/compose-navigation-deep-links.html
  48. Deeplinking val navController = rememberNavController() DisposableEffect(Unit) { ExternalUriHandler.listener = {

    uri -> navController.navigate(NavUri(uri)) } onDispose { ExternalUriHandler.listener = null } } TheGameDbTheme { NavHost(navController = navController, startDestination = Routes.Home{ ... } } Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/compose-navigation-deep-links.html
  49. Deeplinking struct ContentView: View { var body: some View {

    ComposeView() .ignoresSafeArea() // Compose has own keyboard handler .onOpenURL(perform: { incomingURL in handleIncomingURL(incomingURL) }) } } private func handleIncomingURL( _ uri: URL){ ExternalUriHandler.shared.onNewUri(uri: uri.absoluteString) } Source: h tt ps://www.jetbrains.com/help/kotlin-multipla tf orm-dev/compose-navigation-deep-links.html
  50. Conclusions • Kotlin is a Polyglot: It is a fi

    rst-class language for Android, Backend, Desktop, and Web. • KMP's Power: You control the sharing—from logic to the entire UI. • Actual and Expected functions permit a inte rf ace-like approach for speci fi c pla tf orm code. • Resources in Compose Multi Pla tf orm work almost like in Android. • A full Compose Android app is probably multipla tf orm-ready. • Handling DeepLinking on iOS can be tricky.