10 сент. 2025 г.·8 мин чтения

Миграция на Jetpack Compose для уже выпущенного Android-приложения

Миграция на Jetpack Compose не требует полного переписывания. Узнайте, как совмещать экраны Compose и XML, безопасно выпускать фичи, тестировать изменения и снижать риски.

Миграция на Jetpack Compose для уже выпущенного Android-приложения

Почему команды застревают на переписываниях интерфейса

Полное переписывание интерфейса выглядит аккуратно на бумаге. Но реальные приложения устроены сложнее. У вас уже есть пользователи, дедлайны, багфиксы, даты релизов и запросы от продукта, которые не исчезают только потому, что меняется UI-слой.

Команды чаще всего застревают, когда относятся к миграции на Jetpack Compose как к одному переключателю. Тогда обычный roadmap превращается в стоп-кран. Новые фичи откладываются. Мелкие исправления копятся. Продакт-менеджеры начинают спрашивать, почему команда переписывает уже работающие экраны, пока запросы клиентов ждут.

Проблема не только в скорости. Большое переписывание одновременно убирает слишком много определённости. Когда команда меняет сразу много экранов, становится труднее понять, что именно вызвало падение, сломанный макет или рост потребления памяти. Code review разрастаются. QA приходится перепроверять слишком многое. Если релиз идёт не так, откат превращается в хаос.

Здесь многие команды делают неправильный вывод. Им кажется, что старый и новый UI не могут жить вместе, поэтому они планируют переход по принципу «всё или ничего». Android не заставляет выбирать именно так. XML и Compose могут долго существовать в одном приложении. В уже существующий экран можно встроить Compose через ComposeView, а при необходимости использовать старые View внутри Compose. Это даёт пространство двигаться по одному экрану или даже по одному блоку за раз.

Для уже выпущенных приложений это особенно важно, потому что старые экраны часто несут в себе годы скрытых исправлений. Страница настроек может выглядеть простой, но в ней могут быть редкие случаи с разрешениями, правки для доступности и события аналитики, которые никто не хочет заново разбирать в спешке. Переписывать всё это одним махом — редко разумная ставка.

Лучше поставить более скромную цель, и это хорошо. Продолжайте выпускать продукт. Переносите UI маленькими шагами. Если команда соберёт один заголовок профиля в Compose и оставит остальной экран как есть, пользователи всё равно получат новую работу, а приложение продолжит развиваться.

Постепенная доставка лучше, чем красивый план переписывания, который так и не доходит до продакшена.

Что переносить первым

Начинайте с того, что в приложении уже меняется чаще всего. Экран, который продуктовая команда трогает каждый спринт, — лучший кандидат, чем стабильный экран, который никто не хочет редактировать. Тогда миграция на Jetpack Compose принесёт пользу сразу, а не будет месяцами висеть в ветке.

Разметьте все экраны по простой схеме: сколько бизнес-ценности они дают и как часто команда их меняет. Checkout-флоу, шаг регистрации или страница выбора тарифа обычно находятся наверху списка, потому что даже небольшие изменения UI там влияют на выручку или конверсию. Старый юридический экран может выглядеть некрасиво, но если его никто не трогает, он может подождать.

Затем найдите общие элементы интерфейса, которые встречаются почти везде. Кнопки, строки ввода, состояния ошибки, лоадеры, верхние панели и маленькие карточки — хорошие первые победы. Если сначала собрать их в Compose, позже экраны будут мигрировать быстрее, потому что базовые части уже готовы. И вы не будете переписывать один и тот же паттерн пять раз.

Ещё одно хорошее место для старта — экран, который создаёт больше всего боли в верстке. Подумайте об экране с вложенными RecyclerView, слишком большим количеством типов элементов или бесконечными XML include. Compose часто делает такие случаи понятнее и дешевле в изменении. Выбирайте боль, которая тормозит реальную работу, а не ту, что раздражает инженеров раз в квартал.

Хорошая первая цель должна быть небольшой. Настройки, редактирование профиля, форма поддержки или одна вкладка внутри более крупной функции обычно подходят хорошо. Команда может выпустить её, извлечь уроки и сохранить стабильный темп релизов.

Перед выбором используйте короткий фильтр:

  • Экран часто используют или он важен для бизнеса.
  • Команда меняет его чаще, чем раз в очень долгое время.
  • Верстка в XML сейчас неудобна.
  • Успех можно измерить простыми вещами, например уменьшением UI-багов или более быстрыми изменениями фич.

Практический пример: не начинайте с главного экрана приложения, если там смешаны ленты, реклама, deep link и эксперименты. Начните с панели фильтра или блока деталей, который к нему привязан. Вы получите реальный опыт работы с Compose, но зона риска останется маленькой.

Если нужен один ориентир, используйте такой: выбирайте самый маленький экран, который команда и так хочет улучшить в этом месяце. Обычно это лучше, чем самый амбициозный.

Задайте правила до того, как тронете код

Миграция на Jetpack Compose становится грязной, когда каждый член команды делает выбор по-своему. На одном экране используются аргументы Fragment, на другом state берётся напрямую из репозитория, а на третьем цвета XML смешиваются со значениями темы Compose. Этого дрейфа можно избежать, если задать несколько правил до того, как сдвинется первый экран.

Сохраните один и тот же путь навигации на всём протяжении перехода. Не делите приложение на «новое» и «старое». Оставьте тот же nav graph, те же deep link и те же правила back stack. Если экран деталей продукта переедет в Compose, пользователь всё равно должен попадать на него из тех же мест и возвращаться тем же способом.

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

У state должен быть понятный владелец. Самое простое правило такое: ViewModel владеет состоянием экрана, а UI его читает. XML и Compose могут оба наблюдать одно и то же состояние во время перехода, но только один UI-слой должен отвечать за рендеринг конкретного экрана. Если Fragment размещает ComposeView, считайте Compose основным UI этого экрана. Не держите половину виджетов в XML и половину в Compose без очень краткосрочной причины и чёткого конца.

С темой оформления нужна та же дисциплина. Один раз зафиксируйте цвета, отступы, стили текста и правила компонентов, а затем сопоставьте их и для XML, и для Compose. Если старое приложение использует один оттенок синего, а в Compose появляется чуть другой, всё приложение будет ощущаться как собранное из кусочков, даже если код чистый.

Сразу решите, что пока останется в XML. Запишите это, чтобы никто случайно не «мигрировал» это позже. Хорошие кандидаты на временное оставление без изменений:

  • экраны, связанные с потоками WebView
  • шаги оплаты или подтверждения личности со строгими правилами QA
  • большие экраны на RecyclerView, которые уже хорошо работают
  • малопосещаемые админские экраны, которые никто не планирует скоро менять

Короткий письменный договор по миграции экономит переделки позже. Он также ускоряет code review, потому что команда оценивает изменения по правилам, а не по личному вкусу.

Как по шагам смешивать старые и новые экраны

Самая безопасная миграция на Jetpack Compose начинается с малого. Выберите один экран, который часто меняется, или даже только одну его часть, и оставьте остальное как есть. Команды попадают в неприятности, когда переписывают слишком много сразу, а потом неделями чинят детали, о которых пользователи даже не просили.

Если существующий экран уже работает, сохраните XML-разметку и добавьте ComposeView только туда, где это помогает. Новый промо-блок, строка фильтра, блок настроек или компоновщик комментариев часто уже достаточны. Навигация, разрешения, аналитика и deep link могут остаться в старом экране, пока новый кусок доказывает свою ценность в продакшене.

Перед тем как заменять больше UI, вынесите state экрана из слоя представления. Состояние загрузки, сообщения об ошибке, данные формы и действия кнопок разместите во ViewModel или presenter. Тогда и XML-view, и composable смогут читать одно и то же состояние, поведение останется一致ным, а бизнес-правила не расколются на две версии.

Низкорисковая последовательность обычно выглядит так:

  • Сначала замените один изолированный виджет или секцию.
  • Общий state оставьте для XML и Compose.
  • Переходите к следующей секции только после того, как первая стабилизируется.
  • Переводите весь экран, когда большая часть UI уже собрана в Compose.

Есть и обратный путь. Если новый экран на Compose всё ещё зависит от старого виджета, используйте AndroidView и продолжайте движение. Это часто встречается с WebView, MapView, превью камеры, ad SDK views или кастомным графиком, который команда пока не хочет переписывать. Смешивать Compose и XML часто быстрее, чем заставлять себя делать идеальное переписывание.

Небольшой пример делает это понятнее. Допустим, экран checkout всё ещё в XML, но выбор скидки меняется каждую неделю. Перенесите этот выбор в ComposeView, оставьте форму оплаты и код трекинга на месте и понаблюдайте за ошибками в течение одного-двух релизов. Если всё идёт хорошо, затем перенесите блок сводки заказа.

Для экранов, связанных с выручкой, добавьте feature flag. Checkout, paywall, onboarding и восстановление доступа не должны превращаться в драму. Флаг позволяет сначала показать новый UI внутренним пользователям, потом небольшой доле клиентов и быстро выключить его, если метрики ухудшатся.

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

Реалистичный пример миграции

Привлеките Fractional CTO
Получите senior-поддержку по архитектуре Android, продуктовым компромиссам и темпу разработки

Представьте приложение, которым люди пользуются каждый день. Логин-флоу стабилен. Пользователи могут войти, сбросить пароль и пройти двухфакторную проверку почти без жалоб. Больше всего проблем создаёт страница настроек. Она росла годами, собирала странные хаковые решения для верстки и теперь любое маленькое изменение превращается в полдня исправлений.

Такое разделение даёт разумную отправную точку для миграции на Jetpack Compose. Оставьте логин в XML. Он уже работает, а изменения только увеличат риск в той части приложения, которая блокирует каждого пользователя.

Вместо этого сначала переносите страницу настроек.

Оставьте логику, замените экран

Самый безопасный вариант такого перехода меняет UI и оставляет бизнес-логику на месте. Если в приложении уже есть SettingsViewModel, продолжайте использовать его. Те же saved state, вызовы репозитория, правила валидации и события аналитики могут остаться там, где они были.

Старый Fragment по-прежнему может отвечать за навигацию и lifecycle. Внутри него добавьте ComposeView и отрисуйте новый UI настроек там. Пользователь увидит свежий экран, но приложение всё так же будет общаться с тем же ViewModel, что и раньше. Это сильно снижает риск.

Если одна часть настроек всё ещё зависит от более старого custom view, оставьте его пока. Смешивать Compose и XML — нормально во время постепенного Android UI rewrite. Можно встроить Compose в экран через ComposeView, а если нужно, перенести старый виджет в Compose с помощью AndroidView.

Реалистичная страница настроек может включать:

  • переключатели уведомлений
  • выбор темы
  • данные профиля
  • подключённые аккаунты
  • опасную зону для выхода или удаления аккаунта

После релиза наблюдайте за обращениями в поддержку неделю или две. Пользователи всё ещё путаются в настройках уведомлений? Перестали ли они жаловаться на сломанные отступы, обрезанный текст или непоследовательные элементы? Такой фидбек важнее, чем то, переехал ли уже каждый экран в Compose.

Если новая страница настроек спокойно работает в продакшене, у команды есть доказательство, что смешанные экраны работают. Логин может оставаться в XML, пока не появится реальная причина его трогать.

Как тестировать каждый шаг, не тормозя команду

Слишком медленный план тестирования может убить постепенную перепись так же быстро, как и полное переписывание. Делайте проверки узкими и привязанными к экрану, который вы изменили. Если один человек заменил XML-карточку на версию в Compose, ему не должна требоваться неделя QA, чтобы выпустить это.

Screenshot tests лучше всего помогают отслеживать визуальные расхождения. Используйте их для стабильных состояний, которые люди замечают сразу: пустое состояние, загрузка, ошибка и заполненный контент. Если вы переносите только часть уже выпущенного экрана, например строку списка товаров внутри Fragment, сравните старую и новую версии рядом перед merge.

Оставляйте быстрые проверки маленькими

Короткая проверка релиза лучше, чем огромный тестовый набор, которому никто не доверяет. Для каждого перенесённого экрана каждый раз запускайте один и тот же компактный набор:

  • сравните скриншоты для основных состояний UI
  • откройте экран хотя бы на одном маленьком телефоне и одном большом устройстве
  • проверьте скролл, ввод текста, смену фокуса и экранную клавиатуру
  • сравните время запуска и память с прошлым релизом

Реальные устройства важнее, чем многие ожидают. Compose и XML могут вести себя по-разному, когда открывается клавиатура, когда фокус прыгает между полями или когда вложенный скролл встречается с lazy list. Эмуляторы ловят многое, но настоящие телефоны первыми покажут неприятные баги.

Простой пример: вы переносите форму поиска в Compose через ComposeView, но список результатов остаётся в RecyclerView. На скриншот-тесте экран может выглядеть нормально и всё равно сломаться, когда клавиатура сдвинет контент вверх и скроет кнопку отправки. Пять минут на реальном устройстве обычно это находят.

Следите за продакшеном после релиза

После каждой маленькой миграции проверяйте старт приложения и память, а не только поведение UI. Один новый экран обычно не утопит приложение, но несколько небольших регрессий складываются. Отслеживайте показатели после каждого релиза, чтобы быстро увидеть, какое именно изменение вызвало скачок.

Трекинг падений должен оставаться на уровне экрана. Помечайте отчёты названием экрана, версией приложения и тем, видел ли пользователь XML, Compose или смешанный экран. Когда происходит падение, нужно понимать, что сломался именно profile_edit_compose, а не просто что «выросла общая аварийность Android-приложения».

Такая привычка делает миграцию скучной в лучшем смысле. Малое изменение, маленький тест, понятный сигнал.

Ошибки, которые потом создают больше работы

Получите внешний архитектурный аудит
Свежий взгляд поможет заметить слабые места до того, как вы тронете checkout, login или onboarding

Большая часть боли при миграции начинается тогда, когда одна маленькая перепись экрана превращается в три скрытых проекта. Команда меняет XML на Compose, одновременно трогает навигацию, заменяет токены темы и переписывает state экрана в одном pull request. Когда появляются баги, никто не знает, какое изменение стало причиной.

Делайте каждый шаг скучным. Сначала перепишите UI, или сначала перенесите навигацию, или сначала наведите порядок в state. Одно изменение на экран кажется медленнее в течение недели, но позже экономит много переделок.

Если один pull request меняет сразу несколько вещей, разделите его на:

  • layout экрана
  • маршрут навигации или поведение back stack
  • источник state и загрузку данных
  • тему, отступы и правила компонентов

Ещё одна ошибка — тащить старые привычки в новые composable. Если XML-экран полагался на гигантские Fragment, изменяемое общее состояние или копипасту для форматирования, Compose сам по себе это не исправит. Вы просто получите тот же беспорядок в новом синтаксисе.

Используйте миграцию, чтобы убрать очевидный мусор. Выносите переиспользуемый UI в маленькие composable, не помещайте в них бизнес-правила и удаляйте дублирующийся код представления, пока экран ещё свежо у вас в голове.

У state должен быть один ясный дом. Когда Fragment отвечает за часть состояния экрана, а composable — за остальное, ошибки прячутся в щели между ними. Появляются устаревшие значения, двойные сетевые запросы и кнопки, которые сбрасываются после поворота экрана.

Выберите одного владельца для каждого экрана, обычно существующую ViewModel. Пусть composable отображают state и отправляют события наверх. Если Fragment размещает ComposeView, он не должен одновременно хранить свою собственную параллельную версию того же состояния UI.

Команды также теряют время на лёгких, но малополезных экранах. Переписывание страницы «О приложении» кажется безопасным, но почти ничему не учит в сложных местах. Нагруженный список, форма с валидацией или один общий компонент обычно окупаются быстрее, потому что результат можно переиспользовать на других экранах.

Визуальная полировка часто скрывает проблемы доступности. Экран может выглядеть лучше в Compose и при этом работать хуже для реальных людей. Перед тем как считать работу завершённой, проверьте масштаб текста, контраст цветов, размер touch target, порядок фокуса и подписи для TalkBack.

Один частый провал выглядит так: команда переносит экран checkout и одновременно в том же спринте переходит на Compose Navigation и новую design system. Экран выглядит современнее, но back-логика ломается, события аналитики исчезают, а состояния ошибок расходятся между Fragment и composable. Если бы они оставили старую навигацию, использовали ту же ViewModel и изменили только слой рендеринга, релиз был бы намного безопаснее.

Быстрые проверки перед каждым релизом

Проверьте общие компоненты
Заранее определите переиспользуемые паттерны, чтобы следующие экраны переходили быстрее и с меньшими переделками

Выпускайте только тогда, когда новый экран выполняет основную задачу не хуже старого. При миграции на Jetpack Compose маленькие релизы побеждают. Если изменение сложно объяснить, сложно протестировать или сложно откатить, оно, скорее всего, слишком велико для этого спринта.

Короткая проверка перед релизом помогает команде оставаться честной. И ещё она не даёт переписыванию интерфейса тихо превратиться в заморозку продукта.

  • Убедитесь, что команда всё ещё может выпустить обычную фичу в этом спринте. Если перенесённый экран блокирует несвязанные задачи, разбейте миграцию на меньшие части.
  • Назначьте одного человека, который явно отвечает за каждый перенесённый экран, пока он не стабилизируется в продакшене. При общей ответственности мелкие баги часто остаются лежать без дела, потому что все думают, что исправит кто-то другой.
  • Покройте основной пользовательский путь тестами. Для большинства экранов это значит: открыть, ввести данные, сохранить, обработать один сценарий ошибки и вернуться назад без потери state.
  • Покажите изменение поддержке и продукту до релиза. Несколько скриншотов, короткая заметка о том, что поменялось, и одно известное ограничение сильно снижают путаницу после запуска.
  • Оставьте простой путь отката. Feature flag, старый Fragment под переключателем или один релиз, где обе версии ещё существуют, могут сэкономить часы, если начнут приходить отчёты о падениях.

Небольшой пример: представьте, что вы перенесли экран редактирования профиля в Compose, но старая XML-версия всё ещё работает. Сначала выпустите новую версию под флагом на небольшую группу. Если пользователи столкнутся с ошибкой состояния при повороте телефона или при уходе со страницы и возврате, вы сможете быстро переключиться назад, пока команда исправляет проблему.

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

Если на все пять проверок нельзя ответить «да», не форсируйте релиз. Выпустите меньший кусок, сохраняйте темп доставки и пусть постепенная перепись Android-интерфейса остаётся постепенной.

Что делать дальше вашей команде

Полное переписывание имеет смысл только тогда, когда приложению действительно нужен глубокий продуктовый разворот, график релизов свободен, а команда может выдержать месяцы изменений. У большинства уже выпущенных приложений такой роскоши нет. В таком случае миграция на Jetpack Compose обычно лучше работает как серия маленьких, скучных шагов, которые не останавливают релизы.

Это значит, что вы добавляете Compose там, где он даёт понятную пользу, пока оставляете стабильные XML-экраны в покое и держите оба подхода под одними и теми же правилами дизайна и state. Если всё сделано хорошо, пользователи почти не замечают перехода. Команда замечает, потому что новую работу делать становится проще, а не потому, что поверх старого UI накапливается ещё больше долгов.

Выбирайте следующие три экрана не на интуиции, а с причиной. Достаточно простого roadmap:

  • Начните с низкорискового экрана, например настроек или профиля, где команда может настроить тему, навигацию и базовые паттерны.
  • Затем перейдите к экрану, который уже меняется по продуктовым причинам, чтобы миграция помогала доставке, а не тормозила её.
  • В конце возьмите экран со сложными custom view или логикой адаптеров, где Compose может убрать очевидное трение.

После этих трёх экранов остановитесь и посмотрите, что получилось. Изучите отчёты о падениях, UI-баги, полезность preview, скорость code review и то, как часто разработчикам приходилось связывать ComposeView и AndroidView. Если команда всё ещё борется с теми же границами, исправьте это сейчас, прежде чем разносить проблему по всему приложению.

На этом этапе часто экономит время внешняя помощь. Хороший разбор может заметить то, что команды пропускают, пока заняты релизами: разделённое состояние между старым и новым UI, дрейф темы, неясное владение навигацией или переиспользуемые компоненты, которые должны существовать ещё до переноса следующих экранов.

Если вам нужен именно такой разбор, Oleg Sotnikov может посмотреть план миграции Android в рамках работы как Fractional CTO или advisory. Он работает со стартапами и небольшими и средними компаниями над продуктовой архитектурой, lean-настройкой engineering и практичными планами модернизации, поэтому цель остаётся простой: продолжать выпускать продукт, убрать лишние переделки и мигрировать приложение в разумном порядке.