Kotlin Multiplatform Help

Migrating a Jetpack Compose app to Kotlin Multiplatform

This guide is about migrating an Android-only app to be multiplatform across the whole stack, from business logic to UI. It illustrates common challenges and solutions using an advanced Compose sample. You can follow the commit sequence closely or skim the general migration steps and dive deeper into any part that interests you.

The starting app is Jetcaster, a sample podcast app built for Android with Jetpack Compose. The sample is a fully featured app that relies on:

  • Multiple modules.

  • Android resource management.

  • Network and database access.

  • Compose Navigation.

  • The latest Material Expressive components.

All of these features can be adapted into a cross-platform app using Kotlin Multiplatform and the Compose Multiplatform framework.

To prepare to make your Android app work on other platforms, you can:

  1. Learn how to evaluate your project as a candidate for Kotlin Multiplatform (KMP) migration.

  2. See how to separate Gradle modules into cross-platform and platform-specific modules. For Jetcaster, we were able to make most business logic modules multiplatform, except for some low-level system calls that needed to be programmed separately for iOS and Android.

  3. Follow the process of making business logic modules multiplatform one by one by gradually updating build scripts and code to move between working states with minimal changes.

  4. See how the UI code transitions to a shared implementation: using Compose Multiplatform, you can share most of the UI code in Jetcaster. More importantly, you'll see how to implement this transition gradually, screen by screen.

The resulting app runs on Android, iOS, and desktop. The desktop app also serves as a Compose Hot Reload example: a way to quickly iterate on your UI's behavior.

Checklist for a potential Kotlin Multiplatform migration

The main hurdles for a potential KMP migration are Java and Android Views. If your project is already written in Kotlin and uses Jetpack Compose for the UI, it lowers the complexity of a migration considerably.

Here is a general checklist of what preparations you should consider before migrating a project or a module:

  1. Convert or isolate Java code

  2. Check your Android/JVM-only dependencies

  3. Catch up with modularization technical debt

  4. Migrate to Compose

Convert or isolate Java code

In the original Android Jetcaster example, there are Java-only calls like Objects.hash() and Uri.encode(), along with extensive use of the java.time package.

While you can call Java from Kotlin and the other way around, the commonMain source set, which contains the shared code in a Kotlin Multiplatform module, can't contain Java code. So, when you make your Android app multiplatform, you need to either:

  • Isolate this code in androidMain (and rewrite it for iOS), or

  • Convert the Java code to Kotlin using multiplatform-compatible dependencies.

Another Java-specific library, RxJava, is not used in Jetcaster but is widely adopted. Since it's a Java framework for managing asynchronous operations, it's recommended to migrate to kotlinx-coroutines before starting a KMP migration.

There are guides for migrating from Java to Kotlin as well as a helper in IntelliJ IDEA that can automatically convert Java code and streamline the process.

Check your Android/JVM-only dependencies

While a lot of projects, especially newer ones, may not include much Java code, they often have Android-only dependencies. For Jetcaster, identifying alternatives and migrating to them made up most of the work.

An important step is to build a list of dependencies used in the code you plan to share and ensure that multiplatform alternatives are available. While the multiplatform ecosystem isn't as large as the Java ecosystem, it is expanding rapidly. Use klibs.io as a starting point to evaluate potential options.

For Jetcaster, the list of these libraries was as follows:

  • Dagger/Hilt, a popular dependency injection solution (replaced with Koin)

    Koin is a reliable multiplatform DI framework. If it doesn't meet your needs or the required rewrite is too extensive, there are other solutions. The Metro framework is also multiplatform. It can help ease the migration by supporting interop with other annotations, including Dagger and Kotlin Inject.

  • Coil 2, an image loading library (which became multiplatform in version 3).

  • ROME, an RSS framework (replaced with the multiplatform RSS Parser).

  • JUnit, a test framework (replaced with kotlin-test).

As you go along, you may find small pieces of code that stop working in multiplatform because there is no cross-platform implementation exists yet. For example, in Jetcaster, we had to replace the AnnotatedString.fromHtml() function, which is part of the Compose UI library, with a third-party multiplatform dependency.

It's hard to identify all such cases in advance, so be prepared to find replacements or rewrite code during the migration process. This is why we show how to move from one working state to another in the smallest steps possible. That way, a single issue won't stall your progress when many parts are changing at once.

Catch up with modularization technical debt

KMP allows you to migrate to a multiplatform state selectively, module by module, screen by screen. But for this to work smoothly, your module structure needs to be clear and easy to manipulate. Consider evaluating your modularization according to the high cohesion, low coupling principle, along with other recommended practices for structuring modules.

General advice can be summarized as follows:

  • Separate distinct parts of the app's functionality into feature modules, and keep feature modules separate from data modules, which handle and provide access to data.

  • Encapsulate the data and business logic for a specific domain within a module. Group related data types together, and avoid mixing logic or data across unrelated domains.

  • Prevent outside access to a module’s implementation details and data sources by using Kotlin visibility modifiers.

With a clear structure, even if your project has a lot of modules, you should be able to migrate them to KMP individually. This approach is smoother than attempting a full rewrite.

Migrate from Views to Jetpack Compose

Kotlin Multiplatform provides Compose Multiplatform as a way to create cross-platform UI code. To transition smoothly to Compose Multiplatform, your UI code should already be written using Compose. If you're currently using Views, you'll need to rewrite that code in the new paradigm and using the new framework. This is obviously easier when done in advance.

Google has been advancing and enriching Compose for a long time. Check out the Jetpack Compose migration guides for help with the most common scenarios. You can also use Views-Compose interoperability, but just like with Java code, this code must be isolated in your androidMain source set.

Steps to make an app multiplatform

After the initial preparations and evaluations are done, the general process is:

  1. Migrate to multiplatform libraries

  2. Transition your business logic to KMP.

    1. Start with a module that has the fewest other modules depending on it.

    2. Migrate it to the KMP module structure and migrate to using multiplatform libraries.

    3. Pick the next module in the dependency tree and repeat the process.

  3. Transition your UI code to Compose Multiplatform. When all of your business logic is already multiplatform, transitioning to Compose Multiplatform becomes relatively straightforward. For Jetcaster, we show incremental migration by migrating screen by screen/ We also show how to adjust the navigation graph when some screens have been migrated and some have not.

To simplify the example, we removed Android-specific Glance, TV, and wearable targets from the start since they don't interact with multiplatform code anyway and won't need to be migrated.

Prepare the environment

If you'd like to follow the migration steps or run the provided sample on your machine, make sure you prepare the environment:

  1. From the quickstart, complete the instructions to set up your environment for Kotlin Multiplatform.

  2. In IntelliJ IDEA or Android Studio, create a new project by cloning the sample repository:

    git@github.com:kotlin-hands-on/jetcaster-kmp-migration.git

Migrate to multiplatform libraries

There are a couple of libraries that most of the app's functionality relies on. We can transition their usage to be KMP-compatible before configuring the modules for multiplatform support:

  • Migrate from the ROME tools parser to the multiplatform RSS Parser. This requires accounting for differences between the APIs, one of which is how they handle dates.

  • Migrate from Dagger/Hilt to Koin 4 throughout the entire app, including the Android-only entry point module mobile. This requires rewriting the dependency injection logic according to the Koin approach, but code outside *.di packages remains largely unaffected.

    When you migrate away from Hilt, make sure to clear /build directories to avoid compilation errors in previously generated Hilt code.

  • Upgrade to Coil 3 from Coil 2. Again, relatively little code was modified.

  • Migrate from JUnit to kotlin-test. This concerns all modules with tests, but thanks to the kotlin-test compatibility, there are very few changes needed to implement the migration.

Rewrite Java-dependent code into Kotlin

Now that the major libraries are all multiplatform, we need to eliminate Java-only dependencies.

A simple example of a Java-only call is Objects.hash(), which we re-implemented in Kotlin. See the resulting commit.

But what mostly prevents us from directly commonizing code in the Jetcaster example is the java.time package. Time calculation is almost everywhere in a podcast app, so we need to migrate that code to kotlin.time and kotlinx-datetime to truly benefit from KMP code sharing.

The rewrite of everything time-related is collected in this commit.

Migrating the business logic

Once the primary dependencies are multiplatform, we can choose a module to start with the migration. It can be useful to build a dependency graph of the modules in your project. An AI agent like Junie can easily help with that. For Jetcaster, the simplified graph of module dependencies looked like this:

:mobile .

:core:data .

:core:data-testing ..

:core:domain .

:core:domain-testing ..

:core:designsystem .

This suggests the following sequence, for example:

  1. :core:data

  2. :core:data-testing

  3. :core:domain

  4. :core:domain-testing

  5. :core:designsystem — while it doesn't have module dependencies, this is a UI helper module, so we tackle it only when we're ready to move UI code into a shared module.

Migrate :core:data

Configure :core:data and migrate database code

Jetcaster uses Room as the database library. Since Room is multiplatform starting with version 2.7.0, we only need to update the code to work across platforms. At this point we don't have the iOS app yet, but we can already write platform-specific code that will be called when we set up an iOS entry point. We also add configuration for targets for other platforms (iOS and JVM), to prepare for adding new entry points later.

To switch to the multiplatform version of Room, we followed Android's general setup guide.

  • Note the new code structure, with androidMain, commonMain, iosMain, and jvmMain source sets.

  • Most of the code changes are about creating expect/actual structure for Room and the corresponding DI changes.

  • There is a new OnlineChecker interface that is covering for the fact that we only check for internet connectivity on Android. Until we add an iOS app as a target, the online checker is going to be a stub.

We can also immediately reconfigure the :core:data-testing module to be multiplatform. See the resulting commit. It only requires updating the Gradle configuration and moving to the source set folder structure.

Configure and migrate :core:domain

If all dependencies are already accounted for and migrated to multiplatform, the only thing we have to do is move the code and reconfigure the module.

Similarly to :core:data-testing, we can easily update the :core:domain-testing module to be multiplatform as well.

Configure and migrate :core:designsystem

With only UI code left to migrate, we start transitioning the :core:designsystem module, with the font resources and typography. Apart from configuring the KMP module and creating the commonMain source set, we made the JetcasterTypography argument for the MaterialExpressiveTheme into a composable, encapsulating the calls to multiplatform fonts.

Migrating to multiplatform UI

When all the :core logic is multiplatform, you can start moving UI to common code as well. Once again, since we're aiming for full migration, we're not adding the iOS target yet, just making sure that the Android app works with Compose parts placed in common code.

To visualize the logic that we'll follow, here is a simplified diagram that represents relationships between Jetcaster screens:

Home .

Player

PodcastDetails .

Firstly, we created a shared UI module, for the UI code we're going to make common.

To demonstrate migrating the UI gradually, we'll move screen by screen. Each step will end in a commit that contains the app in a working state, a little closer to a fully shared UI.

Guided by the screens diagram above, we started with the podcast details screen:

  1. The migrated screen will work with the Compose theme still in the Android module. What we need to do:

    1. Update the ViewModel and the corresponding DI code.

    2. Update the resources and resource accessors. While the multiplatform resources library is closely aligned with the Android experience, there are some notable differences that need to be addressed:

      • There are slight differences in how resource files are handled. For example, the resource directory needs to be called composeResources instead of res, and @android:color usages in Android XML files need to be replaced with color hex-codes. See the documentation on multiplatform resources to learn more.

      • The generated class with resource accessors is called Res (as opposed to R on Android). After you've moved and adjusted the resource files, regenerate the accessors and replace the imports for each resource in your UI code.

  2. Migrate the Compose theme. We also provide stubs for platform-specific implementations of color schemes.

  3. Continue with the home screen:

    1. Migrate the ViewModel.

    2. Move code to commonMain in the shared UI module.

    3. Move and adjust references to resources.

  4. To demonstrate another way to atomize the migration, we partially migrated navigation. We can combine screens in common code with an Android native screen. The PlayerScreen is still located in the mobile module, and is included in navigation only for the Android entry point. It is injected into the overarching multiplatform navigation.

  5. Finish by moving everything that is left over:

Now that all the UI code has been made common, we can use it to quickly create apps for other platforms.

Optional: Add a JVM entry point

This optional step helps to:

  • Show how little effort it takes to create a desktop app out of an Android app that's been made completely multiplatform.

  • Showcase Compose Hot Reload, which is currently only supported for desktop targets, as a tool for quickly iterating on a Compose UI.

With all the UI code shared, adding a new entry point for a desktop JVM app is a matter of creating a main() function and integrating it with the DI framework.

Add an iOS entry point

The iOS entry point requires an iOS project that is linked with the KMP code.

Creating and embedding an iOS app in a KMP project is covered in the Make your app multiplatform tutorial.

In the iOS app, we need to connect the Swift UI code with our Compose Multiplatform code. We do that by adding a function that returns a UIViewController with the embedded JetcasterApp composable to the iOS app.

Run the app

In the final state of the migrated app, there are run configurations for the initial Android module (mobile) and the new iOS app. You can run the desktop app from the corresponding main.kt file. Run them both to see the way shared UI works on all platforms!

Final summary

In this migration, we followed general steps for turning a pure Android app into a Kotlin Multiplatform app:

  • Transition to multiplatform dependencies, or rewrite the code where it's not possible.

  • Transform Android modules usable on other platforms into multiplatform modules, one by one.

  • Create a shared UI module for Compose Multiplatform code, and transition to shared UI code, screen by screen.

  • Create entry points for other platforms.

This sequence is not set in stone. It's possible to start with entry points for other platforms, and gradually build the foundation under them until they work. In the Jetcaster example, we chose a clearer sequence of changes that is easy to follow step by step.

If you have any feedback on the guide or demonstrated solutions, create an issue in YouTrack.

05 December 2025