Міграція Android-застосунку з Java на Kotlin
У репозиторії 80 000 строк Java-коду, команда давно знає всі грабли, продукт працює — та тут Google оголошує, що нові Jetpack API розробляються тільки для Kotlin, а частина існуючих отримує Kotlin-first surface. Paging 3, DataStore, WorkManager з корутинами, Jetpack Compose — все це технічно доступно з Java, але з такими адаптерами та обходними шляхами, що розробка перетворюється в боротьбу з інструментами замість вирішення задач.
Міграція з Java на Kotlin — не переписування з нуля. Це поступовий процес, який при правильній організації не зупиняє розробку продукту та не ломає стабільність.
Чому не можна просто натиснути «Convert Java File to Kotlin»
Android Studio вміє конвертувати Java-файли в Kotlin автоматично. Результат — технічно компілюється. Але це не Kotlin, це транслітерація Java на синтаксис Kotlin:
-
varвсюди замістьval— немає immutability -
!!на кожній null-ссилці —NullPointerExceptionпросто переименований наKotlinNullPointerException - Немає data classes — ті ж POJO з геттерами через
field.get() -
objectта companion objects відсутні — static-методи болтаються як extensions - Coroutines нема — залишається AsyncTask або RxJava
- Лямбди виглядають як Java 8 лямбди, але без
SAM conversionдля користувальницьких інтерфейсів
Такий код не дає ніяких переваг Kotlin, тільки додає путаницю. Автоконвертер — інструмент для початку, не для фіналу.
Як виглядає правильна міграція
Інвентаризація перед стартом
Перший крок — повний аудит кодової бази: кількість класів по типах (Activity, Fragment, ViewModel, Repository, Model, Util), покриття тестами, список активно розробляємих модулів vs стабільних, залежності від Kotlin-несумісних паттернів (наприклад, finalize(), певні паттерни з static inner classes).
На основі аудиту будується план: які файли конвертуємо в першу чергу, які трогаємо в останню, де паралельна розробка на Java йде під час міграції.
Стратегія «знизу вверх»
Починаємо з класів без Android-залежностей: моделі даних, утиліти, константи. Java POJO з полями, геттерами та сеттерами перетворюється на Kotlin data class — це миттєва виигра: equals(), hashCode(), toString(), copy() безплатно.
// Було: Java POJO, 60 строк з геттерами/сеттерами
// Стало:
data class UserProfile(
val id: Long,
val name: String,
val email: String,
val avatarUrl: String? = null
)
Потім переходимо до Repository-шару. Тут ключове рішення — як обращаться з async-кодом. Якщо в проекті був RxJava, можливі два шляхи: залишити RxJava (Kotlin з RxJava працює чудово) або мігрувати на coroutines + Flow. Другий шлях стратегічно правильніший, але дорожче в моменте. Для активно розробляємих репозиторіїв робимо coroutines; для стабільних модулів без змін — залишаємо RxJava до наступного великого рефакторингу.
ViewModel-шар: LiveData → StateFlow + SharedFlow. Це не обов'язковий крок, LiveData працює й у Kotlin, але StateFlow ведёт себя передбачуваніше — нема магії з LifecycleOwner, нема observeForever утечок, нема setValue vs postValue путаниці.
Activity та Fragment мігруємо останніми. Там більше всього залежностей, більше всього legacy-коду, та помилки там дорожче всього.
Робота з Java-Kotlin interop
Поки міграція не завершена, Java та Kotlin класи живут рядом. Kotlin вибивает Java без проблем. Java вибивает Kotlin — потрібні аннотації:
-
@JvmStaticдля companion object методів, які потрібні з Java -
@JvmFieldдля полів без геттерів -
@JvmOverloadsдля функцій з default parameters -
@Throws(IOException::class)якщо Kotlin-функція видає checked exceptions
Ігнорування цих аннотацій — частая причина того, що автоконвертований код не компілюється з сусідніх Java-файлів.
Тестування в процесі міграції
Кожен конвертований клас повинен проходити існуючі тесты без змін — це гарантія, що конвертація не сломала логіку. Якщо тестів не було — це момент їх написати, до конвертації, поки логіка зрозуміла з Java-коду. Використовуємо JUnit5 + MockK (для Kotlin-класів) або Mockito (якщо потрібна сумісність з Java-тестами).
CI повинен гонити тесты на кожен PR. Міграція без CI — це хаос: неможливо відстежити, який саме коміт сломав логіку.
Що ще змінюється попутно
При міграції розумно попутно вирішувати накопичений технічний борг: замінити AsyncTask (deprecated з API 30) на coroutines, перейти з SharedPreferences на DataStore, оновити Retrofit до версії з Kotlin suspend-функціями замість Call<T>.
Але «попутно» не означає «всё одразу». Кожне таке змінення — ризик регресії. Складаємо явний список «що робимо в рамках міграції», все інше — в backlog наступних спринтів.
Строки
Залежать від обсягу кодової бази, покриття тестами та того, йде ли паралельна розробка нових фіч.
| Кодова база | Покриття тестами | Оцінка |
|---|---|---|
| до 20 000 строк Java | добре (>60%) | 2–4 тижні |
| 20 000 – 60 000 строк | часткове | 4–8 тижнів |
| 60 000+ строк | низьке | 2–4 місяці |
Оцінка уточняється після аудиту. Вартість розраховується індивідуально.
Міграція — інвестиція. Команда, яка працює на Kotlin з coroutines та StateFlow, закриває задачі швидше, ніж та сама команда на Java з RxJava. Не тому що Kotlin магічно краще, а тому що менше бойлерплейту, кращі інструменти аналізу (KSP vs KAPT, lint-правила Kotlin), та бібліотечна екосистема більше не опирається.







