Migrating Android Apps to Kotlin Multiplatform: UI and Tooling Challenges
Lessons on sharing UI layers while maintaining robust screenshot testing and resource management.
1UI Migration Reality Check
Migrating the UI layer to Kotlin Multiplatform proved surprisingly straightforward compared to the surrounding ecosystem. While composables moved easily into commonMain, the build plugins, resource systems, and screenshot testing tools were tightly coupled to Android library assumptions. The application code was ready for sharing, but the supporting tooling required significant adaptation.
2Decoupling UI Logic First
The author utilized a unidirectional-data-flow library to abstract ViewModels, ensuring UI logic remained in testable Kotlin classes. By removing dependencies on androidx.lifecycle before the migration, the team avoided complex platform-specific unpicking. This architectural decision allowed the presentation layer to remain platform-agnostic, making the transition to multiplatform significantly smoother and more efficient.
3The Myth of Platform Code
Initial estimates suggested 12% of the codebase was Android-specific, but this was misleading. Analysis revealed that 70% of that code consisted of @Preview functions for Android Studio. Once these tooling artifacts were excluded, genuine platform-specific code accounted for less than 4% of the total, reframing the migration as a tooling challenge.
4Managing Platform Seams
After removing previews, only a few platform-specific seams remained, such as BackHandler and PlatformWebView. These were managed using expect/actual patterns within core modules. External interactions, like browser navigation or notification settings, were routed through a single ExternalNavigator interface, ensuring that individual feature modules remained entirely free of platform-specific implementation details.
5Centralized Resource Management
All resources were centralized into a single core:res module using commonMain. While this eliminated duplicate keys and simplified management, it introduced a dependency bottleneck. Because the Compose Multiplatform resources plugin lacked a stable API for library modules, the author implemented a reflective workaround to ensure generated assets were accessible to downstream modules.
6Maintaining Visual Regression Safety
To maintain a safety net, the project uses Roborazzi running under Robolectric within the androidHostTest source set. A custom convention plugin automates the process across 40 modules. This setup ensures that UI components are tested in both light and dark modes, providing consistent visual verification without relying on platform-specific emulator dependencies.
7Resource Syncing Workarounds
Because Robolectric reads resources from test assets, the team had to chain two workarounds to bridge the gap. The first created a stable output location for resources, while the second used a Sync task to copy these assets into the host test environment. This complex orchestration was necessary to ensure screenshot tests functioned.
8Trade-offs and Limitations
This migration strategy currently only validates UI stability on Android via Skia-based rendering. It does not guarantee identical behavior on iOS or web targets. Furthermore, storing 55 MB of PNGs in the repository is a significant cost, mitigated only by recording at half scale to keep the visual regression suite manageable.
9Strategic Migration Lessons
Developers should audit their tooling before changing module types, as swapping library definitions has long-term consequences for tests and previews. Keeping platform code out of the presentation layer is a best practice regardless of multiplatform goals. Proper planning for the ecosystem surrounding the code is ultimately cheaper than discovering integration failures later.