← Nesim Apps

Moving an Android App to Kotlin Multiplatform With AGP 9: Seven Traps

By Nesim Razon, independent Android developer · 2026-11-14

#Dev notes#Android#Jetpack Compose

I moved a small Compose game to Kotlin Multiplatform so it could also run on iPhone. The Kotlin part was easy, the build setup was not. These are the seven traps I hit with Android Gradle Plugin 9 and Compose Multiplatform, in the order I hit them, and the checks that told me nothing else had changed.

1. The application plugin and the multiplatform plugin no longer share a module

With Android Gradle Plugin 9 the Kotlin Multiplatform plugin cannot be applied to a module that also uses the Android application plugin. The shared code needs its own library module that uses the new Android library plugin for multiplatform. The app module stays a normal Android application and depends on it. The Kotlin migration guide also asks for a recent Gradle, 9.1 or newer.

2. Unit tests that quietly stop running

I moved the game rules and their ten unit tests into the shared module, ran the build and got a green result. Then I looked at the log: the Android test task of the shared module said there was nothing to run. With the new plugin the Android host test is off until you switch it on.

Declaring a host test in the Android part of the module fixed it, and the results file then showed ten tests run and none failed. The lesson is to count tests after a migration, not just to look for green.

3. Compose Resources need Android resources enabled

Texts and fonts moved into the shared module as Compose Resources. My screenshot tests then failed with a missing resource error that pointed at a compiled strings file. Adding a preview configuration call in the tests did not help. What fixed it was enabling Android resources in the shared module's Android block, together with including Android resources for the host test.

4. A backslash that survived the move

Android string files escape an apostrophe with a backslash. Compose Resources do not unescape it, so the Turkish statistics dialog showed 2048\'e instead of 2048'e. The fix is to use a plain apostrophe in the copied strings.

I found this only because of the next check below. Nothing crashed and nothing failed, the text was just slightly wrong.

5. Test names Kotlin/Native will not accept

One test had a comma in its backtick name, a harmless thing on Android. When the iOS test target compiled, it stopped with a message that the name contains illegal characters. Rewriting the name without the comma fixed it.

6. A simulator target that is gone

I had listed the iOS simulator targets for both Apple silicon and Intel Macs. The build then reported a dependency resolution failure: the Compose Multiplatform libraries were not published for the Intel simulator target. I removed that target, so the build covers the arm64 simulator and real devices.

7. One DataStore per file

The old Android code used a delegate that created one DataStore per process. In shared code I create the store myself and pass it in, and two stores open on the same file crash the app. So I keep a single instance behind a small holder object on Android, pointing at the same file the old delegate used, so existing saved games stay readable.

How I checked that nothing else changed

The screenshot tests that generate the store images became my regression suite. After the migration I regenerated all sixteen store images and looked at what git reported as modified. Fifteen were identical to the committed files, and the one that differed was the apostrophe bug above.

For saved data I installed the build from the old code, played a few moves, then installed the new build over it without uninstalling. After a full app restart the board, score and best score were the same. Because the data file path and keys are unchanged, that check is cheap and worth doing before any release.

Frequently asked questions

Can I use the Android application plugin and Kotlin Multiplatform in one module with AGP 9?

No. Put the shared code in a library module that uses the Android library plugin for multiplatform, and keep the application module separate.

Why did my shared unit tests not run after the migration?

The Android host test is not enabled by default with the new plugin. Enable it in the module's Android block and check that the test count is what you expect.

Do Compose Resources unescape a backslash before an apostrophe?

No. Use a plain apostrophe in the shared strings files instead of the Android-style escape.

Does the Intel iOS simulator still work with Compose Multiplatform?

In my build the libraries were not resolved for that target, so I dropped it and kept the arm64 simulator and real devices.

More from the blog