Привет, я Михаил Селезнев, андроид-разработчик в компании 1221Systems. Поделюсь своими впечатлениями о Compose — будет интересно коллегам, которые думают о переходе на него. Расскажу, в чем были сложности и что помогло мне быстрее разобраться.
Пару слов о моем бэкграунде: я в профессии больше четырех лет, в 1221Systems пришел полтора года назад на проект по разработке большого приложения для заказа продуктов ритейлинговой сети. Проработал около года, в марте этого года перешел на другой проект — приложение для сотрудников компании.
Само приложение написано на достаточно современном стеке: MVI, Koin, Coroutines + Flow, Jetpack Compose. При этом навигация реализована на фрагментах. В onCreateView возвращаем ComposeView, где в setContent прокидываем composable функцию.
abstract class ComposeFragment : Fragment() { private var composeView: ComposeView? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = ComposeView(requireContext()).also { composeView = it } override fun onDestroyView() { super.onDestroyView() composeView = null } protected fun setContent(content: @Composable () -> Unit) { composeView?.setContent { AppTheme(content = content) } } }
Никаких сторонних библиотек для навигации мы не используем, все транзакции проводим вручную.
Я пришел уже на готовый проект. И до этого с Compose, практически, не был знаком. Только слышал про него на конференциях и читал какие-то статьи на эту тему.
Первая фича: удаление аккаунта
Первая фича, которую мне нужно было реализовать — это удаление аккаунта: один экран с пояснительным текстом и двумя кнопками, прибитыми к низу экрана.

Для начала изучил дизайн и требования, а уже после приступил к реализации.
Создал фрагмент и сделал на него переход: тут никаких сложностей не возникло. Дальше UI — решил начать с кнопок. Так как приложение уже в проде, то я решил, что такие кнопки точно уже где-то используются в других местах, поэтому пошел искать их через Resource Manager.
В случае с View это очень удобный инструмент, который позволяет быстро просмотреть все layout’ы в проекте. Но в случае с Compose он не работает: Resource Manager просто не видит СomposeView.
Начал искать другие варианты и вспомнил про Layout Inspector (далее LI), в котором можно посмотреть, насколько эффективно работает твой UI. Открыл в приложении другой экран с нужной кнопкой и через LI попал в composable функцию, описывающую кнопку.
Конечно, можно было пойти просто по файлам и найти что-то по названию, но на тот момент я еще плохо знал проект, и не хотел тратить много времени на поиск.
Добавил первую кнопку, запустил приложение — кнопка показалась. Появилось первое чувство радости за себя - что-то получилось. Нашел вторую кнопку, добавил, запустил приложение и вот тут уже первое разочарование — кнопки наложились друг на друга.
OutlinedRedButton( text = "Удалить аккаунт", onClick = {} ) GreenButton( text = "Назад", onClick = {} )

В классическом View вопрос решился бы с помощью Constraint Layout, но у нас Compose. Поэтому я пошел читать официальную документацию и обнаружил, что для вертикального расположения элементов, нужно использовать элемент Column. Посмотрел, что он используется также на других экранах и добавил его себе. Перезапустил приложение, и о чудо, кнопки отобразились друг под другом.
Column { OutlinedRedButton( text = "Удалить аккаунт", onClick = { } ) GreenButton( text = "Назад", onClick = {} ) }

Дальше через LI нашел похожий по стилю текст и добавил на свой экран.

Почти то, что нужно! Только кнопки должны быть прибиты к низу экрана.
Тут оказалось все достаточно просто: можно использовать элемент Spacer, который заполнит все пространство между текстом и кнопкой. Дальше дело оставалось за малым: через modifier’ы добавил отступы и получил нужный результат.
Стоит также отметить, что все перечисленное выше было обернуто в элемент Scaffold, который позволяет удобно отобразить TopAppBar. Итоговый код получился таким:
@Composable fun DeleteAccountBody( modifier: Modifier = Modifier, onBackClick: () -> Unit = {} ) { Scaffold( modifier = modifier, topBar = { Toolbar( title = "Управление профилем", onBackClick = onBackClick ) } ) { paddingValues -> Column( modifier = modifier .padding(paddingValues) .padding(horizontal = 16.dp) .fillMaxSize() ) { Spacer(modifier = Modifier.height(16.dp)) Text( text = "Удаление аккаунта", style = AppTheme.typography.h3, color = AppTheme.colors.redPrimary ) Spacer(modifier = Modifier.height(16.dp)) Text( text = "Здесь ты можешь удалить свой аккаунт", style = AppTheme.typography.subhead, color = AppTheme.colors.textSecondary ) Text( text = "После удаления данные будут утеряны", style = AppTheme.typography.subhead, color = AppTheme.colors.textSecondary ) Spacer(modifier = Modifier.weight(1f)) OutlinedRedButton( modifier = Modifier.fillMaxWidth(), text = "Удалить аккаунт", onClick = { } ) Spacer(modifier = Modifier.height(16.dp)) GreenButton( modifier = Modifier.fillMaxWidth(), text = "Назад", onClick = {} ) Spacer(modifier = Modifier.height(46.dp)) } } }
Вторая фича: динамичный список элементов
Следующая фича была поинтереснее - это был экран оценки. Помимо текста и кнопки добавился динамический список элементов, которые приходят с бэка.

В классическом view для реализации такого элемента нужно добавить ресайклер, адаптер, делегат, отрисовать в xml view. В Compose этого всего делать не нужно! Достаточно взять Chip-виджет, который входит в библиотеку Compose Material, во ViewModel получить список элементов, прокинуть на Ui слой и пройти список в цикле. Все! На отрисовку этого элемента я потратил в несколько раз меньше времени, чем если бы делал это на View.
Но и это еще не все. Так как элементы в списке можно выбрать, то хочется, чтобы заливка цветом происходила красиво. И Compose позволяет это сделать буквально парой строчек кода с помощью функции animateColorAsState.
Итоговый код:
@Composable public fun FlowRow( modifier: Modifier = Modifier, mainAxisSize: SizeMode = SizeMode.Wrap, mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, mainAxisSpacing: Dp = 0.dp, crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, crossAxisSpacing: Dp = 0.dp, lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, content: @Composable () -> Unit ) { Flow( modifier = modifier, orientation = LayoutOrientation.Horizontal, mainAxisSize = mainAxisSize, mainAxisAlignment = mainAxisAlignment, mainAxisSpacing = mainAxisSpacing, crossAxisAlignment = crossAxisAlignment, crossAxisSpacing = crossAxisSpacing, lastLineMainAxisAlignment = lastLineMainAxisAlignment, content = content ) }
Недостатки Compose
Спустя пару недель пришло общее понимание, как работает Compose, верстка стала более осознанной, а скорость увеличилась в разы. Но и с неприятными моментами столкнулся на личном опыте.
Баг при добавлении строкового ресурса. Происходит не всегда, но достаточно часто. Добавляешь новую строку, используешь ресурс, запускаешь приложение — а у тебя все строки поехали. Там, где должен быть текст «Номер телефона» показывается «Меню» и т.д. Лечится это только очисткой кеша и удалением приложения с эмулятора.
Обновление. Во многих статьях читал, что нужно стараться обновлять Compose до последней версии. У нас на тот момент использовалась версия 1.2.1, а последняя на тот момент была — 1.5.0. Решил обновить. Это сразу повлекло за собой обновление Gradle плагина, версии котлина и еще нескольких библиотек. И тут столкнулся с проблемой: в приложении есть поле ввода телефона, где используется маска для разбиения телефона на группы цифр. И вот эту работу с маской в Compose сломали еще в версии 1.3.0, а в версии 1.5.0 так и не починили. Причем обнаружили мы это не сразу, а спустя неделю, потому что краш воспроизво��ился только при тапе на поле ввода. Пришлось откатываться назад на версию 1.2.1.
Работа со списками. Для каждого элемента в списке передается уникальный id. В нашем случае, это id элемента, который пришел с сервера. Важно: этот id должен быть уникальным, иначе сразу краш.
Спустя полгода: мои рекомендации
На данный момент с Compose я работаю около полугода. За это время успел многое изучить, во многом разобраться, реализовать около 5 больших фичей. На мой взгляд, переходить на Compose стоит. Затраты времени на изучение и переход очень быстро окупятся скоростью верстки и более современным и дружелюбным UI.
Для изучения самой технологии я рекомендую:
Почитать книгу Jetpack Compose Internals. Она на английском, но читается легко, и это было полезно.
Изучать приложение Now in Android от Google.
Начать писать экраны. Можно начать с небольшого, без различных анимаций и усложнять верстку постепенно, накручивая сверху различные фишки.
Посмотреть Philipp Lackner «The Jetpack Compose Beginner Crash Course for 2023»
