Migrating a Real Android App to Kotlin Multiplatform: Lessons Learned
A deep dive into moving Android codebases to KMP while managing platform-specific dependencies.
1Migration Strategy and Scope
The migration achieved 84% code coverage in commonMain by prioritizing modularity. Success depended less on library support and more on decoupling the codebase from Android-specific APIs. While some layers migrated mechanically in an afternoon, others required weeks of refactoring to remove deep-seated platform coupling that had accumulated over many years of development.
2Dependency Injection as a Shield
Koin serves as the primary mechanism to keep platform-specific code out of shared modules. By depending on interfaces rather than expect/actual declarations, the architecture avoids complex initialization steps. This approach allows each platform to bind its own implementation, effectively isolating platform-specific dependencies while maintaining a clean, testable shared codebase for the application.
3Networking and Data Layers
Ktor and SQLDelight facilitated a smooth transition because they were chosen with multiplatform goals in mind. Networking configurations moved to commonMain with minimal friction, requiring only engine-specific adjustments. SQLDelight proved highly effective, though developers must note that the WebWorkerDriver on the web platform currently operates as an in-memory solution without persistent storage.
4The Hidden Android Dependency Trap
A major failure mode involves Android-only dependencies like Context leaking into shared code. These issues often remain invisible until a second target is added to the CI pipeline. A green Android build is insufficient; implementing a secondary target from the very first module conversion is essential to catch platform-specific leaks early.
5The Challenge of java.time
Replacing java.time was the most difficult aspect of the migration due to its pervasive use in domain models and UI formatting. Kotlinx-datetime serves as the replacement, but it lacks some locale-aware formatting features found in Java. Developers must build custom abstractions to handle date intervals and formatting across different platform-specific implementations.
6Handling Files and Streams
Android file handling does not translate directly to web or iOS environments. The solution involves a lazy, stream-based API that avoids loading entire files into memory. By using a lambda-based approach, the system supports retries and redirects, allowing each platform to implement its own efficient reading mechanism while keeping the shared interface clean.
7Storage and Local Data
SharedPreferences and localStorage appear similar but behave differently when handling complex data. The migration utilized Multiplatform Settings combined with kotlinx.serialization to manage structured values. This design choice highlights the importance of abstracting the actual business need rather than mimicking the specific API structure of a single platform like Android.
8Key Takeaways for Architects
Successful KMP migration requires auditing internal code coupling rather than just checking library compatibility. Relying on dependency injection to bridge platform differences reduces maintenance overhead significantly. Ultimately, defining interfaces based on application requirements rather than platform-specific implementations ensures the codebase remains flexible and resilient across all targeted platforms during the transition.