Перейти к содержанию

Legacy & Refactoring

Работа с legacy-кодом в Android требует сохранять существующее поведение и снижать риск изменений. Рефакторинг меняет внутреннюю структуру без изменения наблюдаемого поведения; миграция заменяет технологию или API и требует проверки их семантики, а также поведения.

Legacy

Legacy-код в Android-проекте

Legacy-код содержит поведение и ограничения, которые трудно безопасно изменить: неясные обязанности, скрытые зависимости, отсутствие тестов или неподдерживаемые библиотеки. Возраст сам по себе не делает код legacy; XML-разметка и RxJava не являются проблемой автоматически.

Типичные признаки: большая Activity / Fragment, смешивающая отрисовку, бизнес-правила и доступ к данным, глобальное изменяемое состояние, неявные предположения о жизненном цикле и сильная связанность зависимостей.

Начинайте с конкретных проблем: повторяющихся багов, рискованных изменений, трудностей с тестированием или неподдерживаемой зависимости. Стабильный код без понятной выгоды от замены можно оставить как есть.

Characterization tests (тесты текущего поведения) фиксируют то, что реализация действительно делает, включая неожиданные граничные случаи. Они защищают поведение при изменении структуры, но не доказывают, что оно всегда правильно. Обнаруженные баги исправляйте отдельно, явно задавая новое ожидаемое поведение. Ручной чек-лист может дополнять тесты, но менее надёжен для повторных проверок регрессий.

Улучшайте границы там, где это полезно: выносите доступ к данным за репозиторий, храните состояние экрана во ViewModel, делайте побочные эффекты явными. Use case нужен для сложной или переиспользуемой бизнес-логики, а не как обязательная обёртка над каждым вызовом репозитория.

Инкрементальный рефакторинг

Инкрементальный рефакторинг улучшает одну обязанность или границу за раз небольшими проверяемыми изменениями.

  1. Выберите конкретную проблему и определите ожидаемую пользу.
  2. Зафиксируйте обычные сценарии, ошибки и поведение жизненного цикла точечными тестами.
  3. Создайте seam (точку подмены): заменяемую границу, например зависимость через конструктор или интерфейс вокруг legacy API.
  4. Извлеките или замените одну часть за этой границей, сохраняя контракт для вызывающего кода.
  5. Проверьте поведение, постепенно переведите потребителей, затем удалите старую реализацию.

Например, сначала скройте существующий 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 или явно переключите контекст для блокирующей работы. Не превращайте отмену корутины в обычную ошибку.

Источники