Миграция 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), и библиотечная экосистема больше не сопротивляется.







