Android App Migration from Java to Kotlin
Repository has 80,000 lines of Java code, team knows all the pitfalls, product works — and then Google announces new Jetpack API developed only for Kotlin, and some existing ones get Kotlin-first surface. Paging 3, DataStore, WorkManager with coroutines, Jetpack Compose — all technically available from Java, but with such adapters and workarounds that development becomes fighting tools instead of solving tasks.
Migration from Java to Kotlin is not rewriting from scratch. It's gradual process that, properly organized, doesn't stop product development and doesn't break stability.
Why You Can't Just Click "Convert Java File to Kotlin"
Android Studio can convert Java files to Kotlin automatically. Result compiles technically. But this is not Kotlin, it's Java transliterated to Kotlin syntax:
-
vareverywhere instead ofval— no immutability -
!!on every null reference —NullPointerExceptionjust renamed toKotlinNullPointerException - No data classes — same POJO with getters via
field.get() -
objectand companion objects missing — static methods hanging around as extensions - No Coroutines — stays AsyncTask or RxJava
- Lambdas look like Java 8 lambdas, but without
SAM conversionfor custom interfaces
Such code gives no Kotlin advantages, only adds confusion. Auto-converter is tool for start, not finish.
What Correct Migration Looks Like
Inventory Before Start
First step — full codebase audit: class count by type (Activity, Fragment, ViewModel, Repository, Model, Util), test coverage, actively developed modules vs stable, dependencies on Kotlin-incompatible patterns (eg finalize(), certain static inner class patterns).
Based on audit, plan is built: which files convert first, which last, where parallel Java development happens during migration.
"Bottom-Up" Strategy
Start with classes without Android dependencies: data models, utilities, constants. Java POJO with fields, getters, setters becomes Kotlin data class — instant win: equals(), hashCode(), toString(), copy() free.
// Was: Java POJO, 60 lines with getters/setters
// Became:
data class UserProfile(
val id: Long,
val name: String,
val email: String,
val avatarUrl: String? = null
)
Then move to Repository layer. Key decision here — how to handle async code. If project had RxJava, two paths possible: keep RxJava (Kotlin with RxJava works beautifully) or migrate to coroutines plus Flow. Second path is strategically correct, but more expensive now. For actively developed repositories do coroutines; for stable modules without changes — leave RxJava until next major refactor.
ViewModel layer: LiveData → StateFlow plus SharedFlow. Not mandatory, LiveData works in Kotlin, but StateFlow behaves more predictably — no magic with LifecycleOwner, no observeForever leaks, no setValue vs postValue confusion.
Activity and Fragment migrate last. Most dependencies, most legacy code, most expensive mistakes.
Java-Kotlin Interop Work
While migration incomplete, Java and Kotlin classes coexist. Kotlin calls Java problem-free. Java calls Kotlin needs annotations:
-
@JvmStaticfor companion object methods needed from Java -
@JvmFieldfor fields without getters -
@JvmOverloadsfor functions with default parameters -
@Throws(IOException::class)if Kotlin function throws checked exceptions
Ignoring these annotations — common reason auto-converted code doesn't compile from neighboring Java files.
Testing During Migration
Each converted class must pass existing tests unchanged — guarantee conversion didn't break logic. If no tests existed — this moment to write them, before conversion, while logic clear from Java code. Use JUnit5 plus MockK (for Kotlin classes) or Mockito (if Java test compatibility needed).
CI must run tests on each PR. Migration without CI is chaos: can't track which commit broke logic.
What Else Changes Along the Way
When migrating, reasonable to resolve accumulated technical debt: replace AsyncTask (deprecated from API 30) with coroutines, move from SharedPreferences to DataStore, update Retrofit to version with Kotlin suspend functions instead of Call<T>.
But "along the way" doesn't mean "all at once." Each change — regression risk. Make explicit "what we do in migration scope," rest — in next sprints' backlog.
Timeline
Depends on codebase volume, test coverage, whether parallel development of new features happens.
| Codebase | Test Coverage | Estimate |
|---|---|---|
| up to 20,000 lines Java | good (>60%) | 2–4 weeks |
| 20,000–60,000 lines | partial | 4–8 weeks |
| 60,000+ lines | low | 2–4 months |
Estimate clarified after audit. Cost calculated individually.
Migration is investment. Team working Kotlin with coroutines and StateFlow closes tasks faster than same team on Java with RxJava. Not because Kotlin magically better, but less boilerplate, better analysis tools (KSP vs KAPT, Kotlin lint rules), and library ecosystem no longer resists.







