Legacy & Refactoring
Работа с legacy-кодом в Android требует сохранять существующее поведение и снижать риск изменений. Рефакторинг меняет внутреннюю структуру без изменения наблюдаемого поведения; миграция заменяет технологию или API и требует проверки их семантики, а также поведения.
Legacy
Legacy-код в Android-проекте
Legacy-код содержит поведение и ограничения, которые трудно безопасно изменить: неясные обязанности, скрытые зависимости, отсутствие тестов или неподдерживаемые библиотеки. Возраст сам по себе не делает код legacy; XML-разметка и RxJava не являются проблемой автоматически.
Типичные признаки: большая Activity / Fragment, смешивающая отрисовку, бизнес-правила и доступ к данным, глобальное изменяемое состояние, неявные предположения о жизненном цикле и сильная связанность зависимостей.
Начинайте с конкретных проблем: повторяющихся багов, рискованных изменений, трудностей с тестированием или неподдерживаемой зависимости. Стабильный код без понятной выгоды от замены можно оставить как есть.
Characterization tests (тесты текущего поведения) фиксируют то, что реализация действительно делает, включая неожиданные граничные случаи. Они защищают поведение при изменении структуры, но не доказывают, что оно всегда правильно. Обнаруженные баги исправляйте отдельно, явно задавая новое ожидаемое поведение. Ручной чек-лист может дополнять тесты, но менее надёжен для повторных проверок регрессий.
Улучшайте границы там, где это полезно: выносите доступ к данным за репозиторий, храните состояние экрана во ViewModel, делайте побочные эффекты явными. Use case нужен для сложной или переиспользуемой бизнес-логики, а не как обязательная обёртка над каждым вызовом репозитория.
Инкрементальный рефакторинг
Инкрементальный рефакторинг улучшает одну обязанность или границу за раз небольшими проверяемыми изменениями.
- Выберите конкретную проблему и определите ожидаемую пользу.
- Зафиксируйте обычные сценарии, ошибки и поведение жизненного цикла точечными тестами.
- Создайте seam (точку подмены): заменяемую границу, например зависимость через конструктор или интерфейс вокруг legacy API.
- Извлеките или замените одну часть за этой границей, сохраняя контракт для вызывающего кода.
- Проверьте поведение, постепенно переведите потребителей, затем удалите старую реализацию.
Например, сначала скройте существующий Rx API за репозиторием, а уже потом меняйте состояние экрана или отрисовку. Адаптер позволяет новым потребителям использовать другой контракт, пока старые продолжают работать.
В Android проверяйте поворот экрана, уход и возврат через навигацию, восстановление после пересоздания процесса, deep links, работу без сети, ошибки и дублирование запросов или событий аналитики, если это актуально. ViewModel переживает изменения конфигурации, но восстановление после пересоздания процесса требует подходящего сохранённого состояния или постоянного хранилища.
Разделяйте исправления багов и изменения структуры так, чтобы их можно было проверить независимо. Измеряйте ожидаемый результат: например, снижение числа падений или упрощение тестов. При рискованной миграции функции поэтапный rollout и временный feature flag помогают вернуть старый путь; необратимые изменения данных требуют отдельного плана восстановления.
Migration
Миграция с XML/RxJava на Compose/Flow
Замена UI и реактивного стека - независимые задачи. Сначала стабилизируйте контракты состояния и событий, затем заменяйте по одной реализации.
Views → Compose:
- Добавьте
ComposeViewв существующий Fragment. ИспользуйтеViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, чтобы связать композицию с жизненным циклом представления Fragment. - Переиспользуйте старый
ViewчерезAndroidView; для существующей XML-разметки с View Binding используйтеAndroidViewBinding. - Сохраняйте независимость
ViewModel+UiStateот отрисовки. Собирайте состояние черезcollectAsStateWithLifecycle()в Compose илиrepeatOnLifecycle()сviewLifecycleOwnerво Fragment. - Проверяйте восстановление состояния, фокус, доступность, прокрутку и навигацию. Замена UI-инструментария не сохраняет их автоматически.
RxJava → корутины/Flow:
- Однократный
Single<T>обычно соответствует suspend-функции, возвращающейT;Completable- функции, возвращающейUnit. Используйте подходящий модуль интеграции:kotlinx-coroutines-rx2илиkotlinx-coroutines-rx3. - Адаптируйте потоки на границах слоёв, затем проверяйте время жизни подписки, холодное/горячее поведение, буферизацию/backpressure, ошибки и контекст выполнения.
StateFlowхранит текущее состояние и объединяет обновления; он не является прямой заменой любого Rx-потока или канала событий. - Сбор с учётом жизненного цикла останавливает и заново создаёт подписки. Уточните, должна ли работа источника перезапускаться или оставаться общей для потребителей.
Минимальный адаптер для RxJava 3; LegacyApi и User - типы приложения:
import kotlinx.coroutines.rx3.await
class UserRepository(private val api: LegacyApi) {
// LegacyApi.loadUser(id) returns Single<User>.
suspend fun loadUser(id: String): User = api.loadUser(id).await()
}
await() приостанавливает корутину без блокировки и освобождает подписку при отмене ожидающей корутины. Освобождение подписки останавливает работу источника только при поддержке отмены самим источником. Адаптер не переносит блокирующую работу при подписке в фоновый поток: сохраните подходящие Rx scheduler или явно переключите контекст для блокирующей работы. Не превращайте отмену корутины в обычную ошибку.