Представьте: теплый осенний вечер, на столе чашечка чего-нибудь вкусного, за окном порхают пожелтевшие листья и тонко насвистывает ветер. Но на душе скребутся коварные мыши. И вам точно известно, откуда эти мыши растут: еще летом вышел стабильный Jetpack Compose, а вы таки не затащили его в ваши продакшн-приложения.  И тут в вашу светлую голову забредает шальная мысль: “Почему бы не начать творить что-нибудь эдакое, великое и прекрасное, чтобы все ахнули в восхищении и увидели, какой вы замечательный сотрудник?” Обычно после таких потрясающих идей есть варианта развития событий: либо в проекте действительно появляется нечто прекрасное, либо всё становится просто ужасным. А вот как получилось у нас, я вам сегодня и расскажу.  Всем привет! Меня зовут Паша Стрельченко, я Android-разработчик в hh.ru. В этой статье поведаю историю о том, как начиналась адаптация Jetpack Compose в нашем продакшн-приложении.  Мы поставили перед собой довольно амбициозную задачу: реализовать все компоненты дизайн-системы на Compose. Причем не просто абстрактно реализовать, а именно внутри проекта hh.ru. И разница здесь существенная. А заключается она в следующем: игрушечные playground-ы — это конечно хорошо, они помогают изучить новую технологию, фреймворк, попробовать различные вариации и так далее, но они не имеют никакого отношения к вашему настоящему проекту, внутри которого уже существует большой багаж других технических решений, подключенных библиотек и многого другого.  Поэтому, когда вы только-только затаскиваете тот же Jetpack Compose в ваш проект, могут возникнуть сложности как с гредлом (Gradle). Например, у вас может быть: Маленькая или не совсем та версия, которая поддерживается Jetpack Compose. Не та версия Android SDK. Например, для Jetpack Compose 1.1 требуется minCompileSdk 31, а поднять такую версию соответственно это определённый объем работы. Не та версия Kotlin. И поднятие версии Kotlin — не настолько быстрый процесс, как хотелось бы.  Плюс ко всему еще и с самим Jetpack Compose могут возникнуть сложности. Поэтому первая задача, которую нужно действительно решать — это просто запустить ваш проект с подключенным Jetpack Compose.  Экспериментируя с реализацией различных View, я решил опробовать работу с accessibility в Compose. Написал код, запустил Talkback (специальная утилита для accessibility, для незрячих людей, которые щелкают на определенный элемент экрана, и этот talkback говорит им на что нажали) и... приложение начало крашится при старте. Я долго не мог понять в чём дело, потому что выбрасываемое исключение было не очень понятным.  А дело было в том, что некоторые версии AndroidX библиотек были ниже рекомендуемых для взаимодействия с Jetpack Compose. Но для того чтобы это понять, пришлось потратить довольно много времени. В общем, подключите Jetpack Compose к вашему приложению и проверьте, все ли версии библиотек у вас правильные, хватает ли вам версии Gradle, все ли хорошо с версией Kotlin-а и так далее.  У нас не было особых проблем, потому что мы своевременно обновляем версии Gradle, Kotlin и Android. Поэтому нам достаточно было написать маленький convention-плагин, который умеет подключать Jetpack Compose в конкретный модуль, а не на весь проект сразу. Так и поступили, и у нас всё завелось.   Если коротко и просто, то дизайн-систему создают дизайнеры, чтобы переиспользовать некоторые кусочки макетов. Сюда могут входить как отдельные атомы, например: цвета, шрифты, отступы, тени, так и готовые компоненты с кучей разных вариаций вроде баннеров, которые могут быть самых разных цветов. Туде же можно отнести всякие кнопочки с огромным количество вариаций и многое другое.  Наши дизайнеры уже давно оформили нам необходимые атомы дизайн-системы, есть странички в Figma с цветами, типографикой, иконками, базовыми отступами. Не забыли они и про страницы с отдельными компонентами, например, баннерами: Или, например, кнопки: Почти каждый компонент дизайн-системы имеет несколько разных вариантов. Дизайн-система в hh.ru — довольно увесистая конструкция с разными компонентами. Она действительно обширная, и мы потратили массу времени, когда реализовывали её на обычных xml-вьюшках. Сегодня у нас есть один большой и жирный модуль, внутри которого обитают: большое количество xml-ресурсов, тьма Kotlin-овских файлов, которые обеспечивают работу с этими ресурсами и так далее, и тому подобное. Я помню этот модуль совсем крохой, но за пару лет он превратился в довольно крупную особь с десятками компонентов.  Мы старались сделать всё то же самое на Jetpack Compose. Один доблестный разработчик запилил отдельный модуль design system compose, начал набрасывать в него атомы и некоторые готовые компоненты, но довольно скоро понял, что в одиночку затащить такой объем работ — слишком объемная и практически нереализуемая задача. Вернее, задача-то реализуемая, но прежде чем все эти компоненты будут реализованы, Jetpack Compose уже несколько раз обновится, выпустит сто пятьдесят стабильных версий, и к этому моменту придется всё переписывать. Какие-то компоненты получалось сделать довольно легко, а над некоторыми приходилось повозиться: например, наши кнопки не совсем вписывались в стандартный компоузовский Button (хотя бы из-за наших стилей текста, которые тоже не матчатся на MaterialTheme, и кастомных минимальных размеров), и пришлось реализовывать их с нуля на Surface-е. Banner-ы тоже отняли время, потому что из-за специфичных отступов у разных элементов я всё никак не мог решить, стоит ли делать его на Constraint-е (он в Compose-е выглядит слегка неудобно), или же попробовать сделать на обычных Column-ах и Row.   В общем, доблестный, но удрученный таким раскладом разработчик пошел к тимлиду за советом. И тот подсказал неплохое решение: чтобы не потерять все созданные наработки и донести ценность до конечного продукта, можно попробовать перевести на Jetpack Compose простенький экранчик. Так мы получим первое представление о создании экранов на Jetpack Compose и поймем, как будет работать связка обычных фрагментов и компоузовского кода.  Это важно, потому что мы стопудово не сможем: Сразу отказаться от всего фреймворка фрагментов. Сразу перейти на навигацию на компоузовских функциях. Поэтому мы будем по-прежнему использовать обычные фрагменты, внутри которых будет компоузовский контент. А в конце сделаем еще и демо для команды, чтобы она составила своё мнение на этот счет и дала фидбэк. В качестве первой жертвы мы выбрали экран о приложении. Во-первых, он очень простой — без всякой там бизнес-логики и прочего. Состоит просто из захардкоженной модельки, которая отправляется на рендер во фрагмент.  Как видите, ничего сверхъестественного. Во-вторых, несмотря на простоту, экран всё еще использует часть дизайн-системы: цвета, шрифты, кастомные вьюшки, ячейки для RecyclerView, и так далее. Короче говоря, для этого экрана в любом случае придется что-нибудь реализовать.  И в-третьих, это не самый популярный экран в нашем приложении, на него редко приходят. В приложении с многомиллионной аудиторией это важно, поскольку, если на что-то навернется на одном из главных экранов, и начнутся краши, то их количество очень быстро перевалит за сотни тысяч. А это, как вы понимаете, очень плохо. С жертвой определились, пора переводить. Перевели, визуально сравнили две получившиеся картиночки — как будто разницы нет, поэтому можно продолжать рассматривать общий процесс.  Отличия точно есть, и даже больше пяти =)  Но что конкретно мы делали? Для начала кратко расскажу про схему работы этого экрана. Как я уже писал выше, она максимально проста: у нас был фрагмент, который в специальном коллбэке onCreateViev настраивал некоторые вьюшки и адаптер для  RecyclerView, также была ViewModel, генерирующая специальный UiState и отправляющая ее во фрагмент. А фрагмент, в свою очередь, брал этот State и отправлял его в RecyclerView на рендер.  Типичный экран на RecyclerView. Итак, что мы сделали. Во-первых, мы почти не трогали существующую ViewModel. Внутри нее, до перевода на Compose, был специальный UiConverter, который принимал на вход захардкоженную модель и отдавал список элементов для RecyclerView.  ManualStateViewModel — это класс из нашего фреймворка, который отличается от обычной ViewModel наличием rx-ового BehaviorSubject-а под капотом для подписки на UiState. После того, как мы перевели этот экран на Compose, нам этот UI на контейнере стал не нужен, и мы начали отправлять захардкоженную модель прямо на экран — на последующий рендер.  UiConverter для этого экрана стал бесполезен, но это не значит, что он будет не нужен на всех остальных экранах: иногда требуется отобразить на множестве разных экранов один и тот же кусочек UI, тогда конвертеры могут смапить domain-сущность в одну и ту же ui-модель. Во-вторых, мы немножечко почистили фрагмент. Раньше у нас в нем были настройки вьюшек, RecyclerView-адаптера и специальный метод, который назывался RenderState. Он принимал на вход UiState и отправлял ячейки в RecyclerView.  После перевода на Compose в методе onCreateView у нас осталось только создание компоузовской вьюшки, а метод renderState немного преобразился. Внутри него мы вызываем метод setContent, чтобы связать мир XML с миром Jetpack Compose. И вот туда мы отправляли ту захардкоженную модельку, которую нам нужно. Таким образом наш фрагмент немного похудел, из него исчезла настройка вьюшек.  На состояние 27.07.2022 этот код уже сильно изменился, мы накрутили дополнительных абстракций для упрощения настройки, но это уже другая история. Последнее, что мы сделали — верстку секций экрана на Compose. До перехода у нас был простой экран с Toolbar и RecyclerView. Всё это было обернуто в LinearLayout.  Я же говорил! Простой экран. После перехода на Compose кода стало в разы больше, и кажется, что стало сложнее. Однако, если присмотреться, каждая из описанных секций экрана максимально проста. В ней очень легко разобраться, для этого совсем не нужно рыскать по кодовой базе, искать детали адаптер-делегатов и вникать, как именно были отрисованы ячейки. Всё прозрачно и прописано в одном файле. Поэтому стало ощутимо проще.  Каждая секция экрана выглядит довольно простой, и так как кода не слишком много, мы оставили их в одном файле. Вот, например, верхняя часть экрана — большая иконка с логотипом, название приложения и описание версии: Код выглядит громоздким из-за большого количества переносов строк, но в самих кусочках нет ничего сложного.  После перевода этого простого экрана на Compose мы сделали следующие выводы: Во-первых, с Compose проще выделять переиспользуемые View. Вспомните, как это было на XML. Там, когда только начинаешь задумываться о выделении какой-нибудь кастомной вьюшки, тебе уже не хочется заморачиваться, ведь для этого нужно создать XML-ный файл под верстку этого файла, сделать Kotlin-класс, который будет inflate-ить или настраивать эту самую вьюшку. А если нужны атрибуты, ты еще и создаешь под них специальный XML-файл, как-то их парсишь, связываешь и так далее. В общем, очень устаешь уже даже просто думать про это всё.  А Compose поощряет переиспользование различных функций. Можно сделать вьюшку, написать несколько вариантов функции с разными параметрами (хвала полиморфизму!), описать простенький маппинг и всё. Теперь можете использовать готовую вьюшку в нескольких частях вашей кодовой базы.  Во-вторых, стало проще делать множество превью. В XML у нас был специальный namespace, который назывался tools. Благодаря ему можно было менять внешний вид вашей View при работе с Layout-дизайнером: цвет background-а, шрифты, цвет текста и многое другое. Однако, чтобы показать несколько превью одной и той же вьюшки с какими-то разными параметрами приходилось заморачиваться. Либо создавать кастомную View, внутри которой делать парсинг разных атрибутов, либо делать копипаст в отдельный временный XML-ный файл: вставлять код, менять атрибуты и смотреть, как выглядит.  В Compose есть специальная аннотация @Preview, благодаря которой вы можете либо взять отдельный кусочек этой вьюшки и отправить ее на устройство, либо сделать несколько функций, отмеченных аннотаций превью и посмотреть, как они смотрятся в Layout-дизайнере вместе. Это довольно удобно, когда вы разрабатываете новый компонент. До недавнего времени аннотация @Preview работала только в application-модулях, но к счастью это исправили. Однако в документации до сих пор не написано, что для корректной работы этой фичи нужно добавлять несколько дополнительных зависимостей в build.gradle. В-третьих, стало гораздо проще композировать готовые виджеты. Для наглядности приведу хороший пример с нашими ячейками для RecyclerView.  Многие уже работали с обычными RecyclerView и делали так называемые Adapter Delegate-ы. Это специальные кусочки будущего адаптера RecyclerView, которые умеют отображать определенный ViewType, чтобы вы могли комбинировать различные ViewType’s в одном списке.  Каждый AdapterDelegate нацелен на определенную модельку элемента списка и умеет инфлейтить и биндить именно ее.  Вероятно, у вас было нечто похожее. Пусть и не идентичное, но идея остается той же.  Для каждого ViewType вы создавали отдельный AdapterDelegate, потом привязывали их к единому Delegate менеджеру, который в итоге превращался в RecyclerView-адаптер. Затем вы привязывали его к RecyclerView и ваш список мог отображать несколько типов.   Но тут наш дизайнер придумал ячейки – Cell-ы. Их идея заключалась в том, что каждая ячейка списка теперь состоит из левой и правой частей. Есть некоторое количество вариантов левой части и некоторое количество вариантов правой. Дизайнеры их очень весело комбинируют и получают огромное количество вариантов для элементов RecyclerView.  Дизайнеры нарисовали 14 вариантов левой части и 7 вариантов правой, всего-то 98 разных ячеек. Мы посмотрели, какое API для работы с ячейками получилось у наших коллег из iOS, и оно нам очень понравилось. Если присмотреться, то они просто объявляли некоторые алиасы к определенным контейнерам, которые умели принимать и левую, и правую часть, а затем проинвентаризовали их двумя дженериками — дженерик левой части и дженерик правой.  Очень классная фича Swift — можно описать generic-тип и потом просто создавать объект указанного типа: Благодаря этому, просто описав typealias можно создать все нужные объекты под капотом ячейки.  Мы решили сделать точно так же, и… Прошли через все пресловутые стадии – отрицание, гнев, торг и далее по списку.  Из состояния “да всё мы сейчас сделаем в два счета” мы перешли к подозрениям, что задача сложнее, чем нам казалось. А затем и вовсе очень сильно расстроились, потому что у нас не получилось сделать ровно то же самое, что у ребят в iOS. А получилось вот что: очень сложный фреймворк поверх RecyclerView, огромное количество интерфейсов, сущности, которые решали разные задачи, биндинг, инфлейт, дифинг и многое другое. В общем, получилось всё до ужаса сложным и каждая ячейка описывалась очень громоздкой конструкцией, в которой было много параметров и дженериков, а еще приходилось дублировать очень много кода. Это был провал. Тонна generic-ов, куча интерфейсов, специальная diffing-стратегия, чтобы корректно сравнивать одну ячейку с другой, множество общих для всех ячеек override-параметров... В таком виде даже простые ячейки описывались довольно сложно и имели некоторые подводные камни в реализации. А на Compose весь наш фреймворк ячеек уместился в 80 строк.  С момента выхода видео в "Охэхэнных историях" ячейки немного усложнились. Во-первых, мы поняли, что нам неудобно проставлять weight в каждой ячейке для растягивания левой части, поэтому вытащили weight в основной каркас. А во-вторых, у нас появилась специальная expandable-ячейка, для которой нужно было убирать click без изменений alpha и семантики.  И этот каркас тоже прекрасно решал свою задачу.  Весь фреймворк написался буквально за один вечер. Здесь есть каркас, который умеет принимать в себя левую и правую части, контролирует общие параметры (“ячейка доступна/недоступна”, “включить/выключить альфу”, “показать/не показать разделитель в списке” и многое другое).   Да и сами ячейки стали попроще. Да, в них по-прежнему осталось большое количество разных параметров, но само содержимое этих ячеек значительно упростилось. Кажется, это большая победа Compose.  В рамках перевода нашего простого экрана на Jetpack Compose мы решили заодно поресерчить, как обстоит вопрос с UI-тестами. Спойлер: их можно писать, и они работают уже сейчас.  В процессе ресерча мы слезли с наших форков Kaspresso и Marathon, на которых сидели аж два года. Они существовали у нас, потому что нам хотелось сделать Allure-отчеты с шагами, но современные версии Kaspresso и Marathon уже поддерживают это прямо из коробки и, более того, полностью поддерживают функциональность работы с Jetpack Compose. Поэтому мы наконец-то пересели на стабильные версии.  Текущая стабильная версия Kaspresso уже давно поддерживает генерацию отчётов с шагами, главное подключить правильный test runner. Мы попробовали написать "гибридные" UI-тесты. Они проходились по части XML-ных экранов, переходили на экран с Jetpack Compose-виджетами, а затем обратно. В общем, немножко потестили навигацию, как работают assert-ы в Compose-контексте, и вроде всё работает.  Кстати, с тестами была одна хитрость — для взаимодействия с Compose-виджетами вам нужен специальный ComposeTestRule. И нужно внимательно выбрать fabric-метод для создания такого Rule-а, потому что можно случайно запустить несколько Activity вашего приложения. В нашем случае подошёл createEmptyComposeRule:  При этом Kakao теперь поддерживает Compose и вы можете писать мини-Page Object-ы для, например, ваших кастомных вьюшек, чтобы упростить жизнь тестировщикам. Всё просто: пишете специальный Page Object для баннера или кнопочки, а тестировщики будут их использовать.   В качестве аналога обычным идентификаторам в Jetpack Compose используется специальный Semantics.testTag. Это те же идентификаторы, по которым можно искать определенные элементы, виджеты, взаимодействовать с ними, и так далее. А еще можно пробрасывать собственные кастомные семантические свойства. Это может потребоваться, например, для сравнения какой-нибудь картинки.  Удивительно, но в данный момент в Jetpack Compose при попытке протестировать ту же картинку, что засетилась, вы не можете не можете достучаться до свойств той картинки, которая вам нужна. Каждый элемент экрана — это просто какая-то нода, у которой нет никаких свойств, и вы можете взаимодействовать только с семантикой. Приходится пробрасывать свои собственные свойства.  Еще в рамках ресерча мы пробовали написать unit-тесты на отдельные виджеты с помощью Kaspresso и Robolectric. Такие тесты могут пригодиться, если вы решите написать сложную кастомную вьюшку, в которой пробрасывается сложная модель. Тогда в рамках unit-тестов получится проверить, правильно ли сетятся те или иные свойства.  Когда мы попытались написать такой unit-тест, у нас всё очень трудно заводилось. Нам приходилось производить непонятные хаки с Gradle-ом, учитывать много непонятных настроек, подключать кучу библиотек. В общем, после всех этих страданий мы пришли к выводу, что на самом деле оно нам не надо, потому что наши UI-тесты делают примерно то же самое.  Итак, мы перевели экран на Jetpack Compose, поресерчили UI-тесты, показали демо команде и решили, что эксперимент был успешным. Дальше мы захотели двигаться в сторону миграции на Jetpack Compose. Но как именно мы будем двигаться?.. Существует несколько стратегий миграции с одной технологии на другую.  Первую можно условно назвать “революция” — это когда вы говорите: “Ребята, с завтрашнего дня мы все пишем на Jetpack Compose”. Да, не у всех есть опыт работы с этой технологией, есть куча нерешенных проблем, вы не знаете, как будете реализовывать тот или иной элемент вашей дизайн-системы. Зато вся команда прокачивается разом и вы сразу пишете на новой технологии. Это быстрый путь, но жестокий, ненадежный и нестабильный. Если завтра будет релиз и понадобится срочно что-то сделать, неизбежно возникнут трудности и проблемы.  Второй путь — “реформа”. Вы выделяете отдельную команду, которая говорит остальным: “Коллеги, пока пишите просто на XML, как привыкли. А мы поделаем все элементы дизайн-системы, решим основную массу проблем и, когда все будет готово, мы всех позовем, чтобы начать адаптацию новой технологии под вас”. И третья стратегия — это нечто среднее между первыми двумя, ее можно назвать “программа раннего доступа” (EAP — early access program). С помощью платформенной команды решаете какую-то часть проблем (но не все), затем выбираете команду, которая готова страдать больше других и просите их писать на новой технологии. Эта команда будет сталкиваться с разными проблемами, будет решать их самостоятельно и, главное, прокачиваться. Другие команды до некоторых пор будут продолжать сидеть на старой технологии, зато когда решит попробовать Compose, будут уже две команды, которые готовы им помогать. EAP — более быстрый путь, чем “реформа”, и более надежный, чем “революция”. Так что мы решили, что будем двигаться по пути от “реформы” к “early access program”. Выделим команду, которая будет собирать все шишки и создавать компоненты дизайн-системы, а при декомпозиции каждого продуктового портфеля, будем звать продуктовую команду и вместе с ними оценивать, насколько готова наша дизайн-система к реальному production-экрану. И если всё отлично, то мы начнем перетаскивать всех разработчиков на Jetpack Compose.  В начале марта мы были сосредоточены на реализации компонентов дизайн-системы. Мы завели специальную доску в Miro, где отслеживали прогресс в реализации этих компонентов и наклеили кучу желтых стикеров, обозначая ещё не реализованые компоненты. Реализованные компоненты помечали зелёным цветом. Стикеры постепенно "зеленели", это грело душу.  Мы понимали, что Jetpack Compose — это молодая технология, поэтому многого из коробки просто нет. Нам не хватило тогда, например, простого XML-флажочка includeFontPadding.  Когда ваши дизайнеры рисуют типографику в Figma, они обычно центрируют текст внутри контейнера с текстом. В Android у нас нет возможности центрировать текст внутри контейнера из коробки. Но есть специальный лайфхак с includeFontPadding и возможностью указать специальное значение firstBaselineToTopHeight и lastBaselineToBottomHeight, чтобы попробовать сделать pixel perfect текста вместе с Figma.  В Compose этого флажочка нет, поэтому если наложить друг на друга экраны "О приложении" на XML и Compose, то станет заметно, что Compose-версия немного отличается от XML.  Если наложить два изображения друг на друга, видим, что надписи на Compose-версии слегка уехали вниз: Душа перфекциониста в этот момент негодует (╯ ° □ °) ╯ (┻━┻)   Все надписи слегка уехали вниз, потому что у шрифтов есть отступы (pagging-и), и убрать их мы пока не можем. Есть некоторые workaround-ы для этой ситуации:  Использовать внутри Compose-вёрстки AndroidView, куда вставить XML-ную TextView, чтобы использовать нужные атрибуты. Подхачить файлы шрифтов, чтобы убрать padding-и и написать немного кода в Modifier-ах. Дождаться версии Compose-а 1.2.0, чтобы наконец-то воспользоваться нормальным решением. Еще нам не хватило нормальной работы с Bottom Sheet-ами. В Material-библиотеке Jetpack Compose есть некоторые виджеты для работы с Bottom Sheet-ами, но они не такие удобные как хотелось бы. Например, если нужно показать несколько Bottom Sheet-ов на одном экране, приходится создавать дополнительные классы для переключения между диалогами. Плюс еще там требуется, чтобы определенный виджет был корнем вашей иерархии — это не всегда удобно. В какой-то момент мы забросили попытки писать Bottom Sheet-ы на Compose, и начали использовать привычные BottomSheetDialogFragment-ы, с той лишь разницей, что вёрстку делали на Compose-е.  Также у нас были проблемы с реализацией CollapsingToolbar. Наши дизайнеры нарисовали замечательный конструктор для тулбара, который называется NavBar Он, аналогично ячейкам, состоит из левой, правой, верхней и нижней частей.  Как и в случае с Banner-ами, кнопками, ячейками — здесь куча вариантов.  Мы думали, что у нас получится так же удобно, как и с ячейками, но, к сожалению, NavBar иногда используется и как CollapsigToolbar. Поэтому, когда мы попробовали реализовать это, у нас сходу не получилось удобного API. Чтобы сделать сколько-нибудь приличную реализацию, нам потребовалось довольно много времени, чтобы реализовать этот компонент. Как я уже сказал, Jetpack Compose — молодая технология, поэтому из коробки чего-то может не хватать. В частности, например, нет такой простой вещи как Linkify. Это когда отправленная вами ссылка в чатике подсвечивается как ссылка, а при нажатии на нее открывается браузер. Этой штуки Compose сегодня просто нет, приходится ее реализовывать руками. Это не очень сложно, но неприятно.  Мы смогли начать адаптацию, поэтому я думаю, что у вас всё тоже получится. Главное — начать с простенького запуска вашего приложения с подключенным Jetpack Compose.  Затем попробуйте перевести хотя бы один простенький экран на Jetpack Compose, оцените риски, насколько сложно будет реализовывать ваши компоненты дизайн-системы, насколько для вас будет трудным взаимодействие между фрагментом и Compose-овским кодом. Все это очень важно проверить на ранней стадии, прежде чем бросаться в глубокий омут этой миграции.  И последняя рекомендация — скорее всего будет проще, если вы уже используете UDF-архитектуру. Если у вас уже есть, например, тот же Presenter или же ViewModel-ка, которая отдает единый UI-стейт на рендер в ваш фрагмент. Тогда вы сможете просто заменить XML-код по рендерингу на вашу Compose-реализацию и будет гораздо проще.    Android-разработчик