Jetpack Compose — как легко построить UI на Android
В июле этого года вместе с Android Studio Arctic Fox вышла одна из долгожданных библиотек — Jetpack Compose. Она позволяет создавать пользовательский интерфейс в декларативном стиле и обещает быть революцией в построении UI.
Разбираемся, так ли это на самом деле, какие у библиотеки преимущества и недостатки. Подробности — в статье.
Преимущества Jetpack Compose
Jetpack Compose — это набор инструментов для разработки UI в Android-приложении. Он призван ускорить и упростить разработку пользовательского интерфейса, избавить от лишнего кода и соединить модель реактивного программирования с лаконичностью Kotlin.
Сразу с места в карьер — какие есть преимущества у библиотеки:
1. Меньше кода. Jetpack Compose позволяет писать меньше кода, а значит разработчик может больше фокусироваться на проблеме, с меньшим количеством тестов и дебага, а значит и багов.
2. Интуитивно понятный. Compose использует декларативный API — разработчику нужно лишь сказать, что сделать, а все остальное ляжет на плечи библиотеки.
3. Удобство внедрения. Compose совместим с любым существующим кодом. Например, можно вызвать Compose-код из вьюх (view) и, наоборот, вьюхи из Compose. Многие библиотеки вроде Jetpack Navigation, ViewModel и Coroutines уже адаптированы под Compose, что позволяет сравнительно быстро внедрить его в свой код. Кроме того, Android Studio Arctic Fox поддерживает превью создаваемых вьюх.
4. Имеет обширный инструментарий. Jetpack Compose позволяет создавать красивые приложения с прямым доступом к Android Platform API и build-in поддержкой Material Design, тёмной темы, анимаций и других крутых штук.
Далее пройдёмся по основным аспектам библиотеки и посмотрим, как сильно повышается производительность приложения.
Подключение к проекту
Чтобы подключить Jetpack Compose к проекту, необходимо указать некоторые строки кода в своем build.gradle.
В рутовом объявим переменную с версией Compose:
Здесь мы указываем, что в проекте будем использовать Jetpack Compose и объявляем необходимые зависимости (подробнее про зависимости можно почитать в официальном гайде).
Дальше всё просто. В активити (activity) объявлем Composable-функцию, строим иерархию вьюх с указанием необходимых атрибутов и смотрим результат.
Пройдемся по коду. Я написал две реализации вёрсток различной сложности:
1. Простая реализация
Добавляет TextView в вёрстку с текстом с конкатенацией Hello и аргумента, переданного в Greeting.
Важно отметить, что имена Composable-функций начинаются с заглавной буквы. Это соглашение по наименованию функций, поэтому если писать со строчной, то студия будет подсвечивать неверный нейминг.
2. Более сложная реализация
Этот вариант представляет собой скролящийся экран, который содержит изображение, текст и кнопку. Рассмотрим некоторые особенности:
Необходимо объявить Scroll State. Только не обычный, а тот, который позволяет сохранять состояние скролла сквозь рекомпозицию — rememberScrollState().
Column представляет собой ViewGroup с вертикальным расположением элементов.
Modifier позволяет управлять атрибутами, добавлять декорации и поведение к вьюхам.
Остальное интуитивно понятно. И это как раз одна из ключевых особенностей Jetpack Compose — даже если вы не использовали библиотеку ранее, то всё равно с ней разберётесь.
Добавить вьюхи в активити можно через extension setContent <>, например:
В общем-то, создание UI выглядит действительно просто. Теперь определим, насколько сильно оптимизируется приложение и как быстро пользователь увидит окончательный экран.
Для тестирования воспользуемся библиотекой Jetpack Benchmark, о которой, кстати, тоже рассказывали в отдельной статье. Код теста выглядит так:
Протестируем три версии установки вьюхи в активити:
При передаче ресурса в setContentView.
При передаче вьюхи в setContentView.
Итоги тестирования можно посмотреть в таблице: левый столбец — название теста, правый — время на выполнение:
Compose. Jetpack Compose
Благодаря стремительному развитию мобильной индустрии каждые несколько лет мы наблюдаем появления новых технических решений, призванных усложнить упростить жизнь разработчикам. Некоторые из них, не сыскав популярности у пользователей, остаются лишь частью истории, другие – плотно укореняются в повседневной разработке, становясь в определенной области стандартом де-факто.
Пожалуй, главным трендом мобильной разработки за последние несколько лет стал декларативный UI. Такое решение уже давно успешно применяется в веб и кроссплатформенных решениях и, наконец, добралось и до нативной разработки. На iOS существует SwiftUI (представленный на WWDC 2019), а на Android – Jetpack Compose (представленный месяцем ранее на Google I/O 2019). И именно о последнем мы сегодня и поговорим.
Примечание: в данной статье мы не будем рассматривать поэтапное создание первого проекта на Compose, так как этот процесс прекрасно описан в других материалах. Моя цель – лишь рассказать о преимуществах и недостатках, которые дает android-разработчикам переход на Jetpack Compose, а решение использовать или нет всегда остаётся за вами.
Появление
Официальная история Jetpack Compose начинается с мая 2019, когда он был представлен публике на конференции Google I/O. «Простой, реактивный и Kotlin-only» – новый декларативный фреймворк от Google выглядел как младший брат Flutter (который к тому моменту уже стремительно набирал популярность).
API design is building future regret
О недостатках текущего UI-фреймворка Android было сказано и написано уже достаточно большое количество раз. Проблемы с View-иерархией, зависимость от релизов платформы – наличие этих и множества других мелких недостатков в той или иной мере доставляли неудобства разработчикам, что и побудило компанию Google заняться разработкой нового фреймворка, способного решить все эти проблемы.
Преимущества
Итак, чем же хорош Jetpack Compose и, главное, чем он кардинально отличается от существующего на данный момент UI-фреймворка Android?
В то же время, при использовании Jetpack Compose, решение будет выглядеть следующим образом:
Ну и напоследок – сравнительный результат:
Недостатки
Декларативный стиль
Отдельное внимание стоит уделить главной особенности Jetpack Compose – декларативному стилю создания UI. Суть подхода заключается в описании интерфейса как совокупности composable-функций (они же виджеты), которые не используют «под капотом» view, а напрямую занимаются отрисовкой на canvas. Для кого-то это минус, для других – возможность попробовать что-то новое. Так или иначе, к концепции «верстать UI кодом» нативному разработчику, не работавшему ранее с аналогичными технологиями (к примеру, Flutter или React Native), придётся привыкать.
Что за Unidirectional Data Flow?
В современном android-приложении UI-состояние меняется в зависимости от приходящих событий (нажатие на кнопку, переворот экрана и т.д.). Мы нажимаем на компонент, тем самым формируя событие, а компонент меняет свой state и вызывает callback в ответ. Из-за довольно тесной связи UI-состояния с View это потенциально может привести к усложнению поддержки и тестирования такого кода. К примеру, возможна ситуация, когда помимо внутреннего state компонента, мы можем хранить его состояние в поле (например во viewmodel), что теоретически может привести к бесконечному циклу обновления этого самого state.
Что же касается Jetpack Compose, то здесь все компоненты по умолчанию являются stateless. Благодаря принципу однонаправленности нам достаточно «скормить» модель данных, а любое изменение состояния фреймворк обработает за нас. Таким образом, логика компонента упрощается, а инкапсуляция состояния позволяет избежать ошибок, связанных с его частичным обновлением. В качестве примера возьмем уже рассмотренный ранее composable-код. Перед описание компонентов были определены две переменные:
Мы создаем два текстовых объекта, значения которых будем устанавливать полям ввода (логина и пароля) в качестве value. А благодаря связке remember любое изменение значений этих объектов (из других частей кода) уведомит об этом соответствующее поле ввода, которое перерисует только значение value, вместо полной рекомпозиции всего компонента.
Погружение в Jetpack Compose
Всем привет. Перед уходом на выходные спешим поделиться с вами еще одним переводом, подготовленным специально для студентов курса «Android-разработчик. Продвинутый курс».
Пробуем новый UI-фреймворк для Android-приложений
В течение последних нескольких лет, участвуя во многих мобильных проектах, мне приходилось использовать различные технологии, например, такие как Android, ReactNative и Flutter. Переключение с ReactNative обратно на классический Android вызвало у меня смешанные чувства. Возвращение к Kotlin прошло хорошо, но я очень скучал по UI-фреймворку React. Небольшие повторно используемые компоненты, с помощью которых создается пользовательский интерфейс, великолепны и обеспечивают большую гибкость и скорость разработки.
Вернувшись в классический Android, мне нужно было беспокоится о том, чтобы сохранить иерархию View как можно более однообразной. Из-за этого трудно по-настоящему посвятить себя компонентному подходу. Это делает копипаст более заманчивым, что приводит к более сложному и менее поддерживаемому коду. В конечном итоге мы удерживаем себя от экспериментов с пользовательским интерфейсом, которые могли бы улучшить UX.
Android раскрывает Jetpack Compose. Иллюстрация: Эмануэль Багилла (Emanuel Bagilla)
Jetpack Compose спешит на помощь
Поэтому после просмотра What’s new in Android с конференции Google I/O 2019 я сразу же начал разбираться с Compose и постарался больше узнать о нем. Compose — это инструментарий реактивного пользовательского интерфейса, полностью разработанный на Kotlin. Compose выглядит очень похоже на существующие фреймворки пользовательских интерфейсов, такие как React, Litho или Flutter.
Нынешняя структура UI-фреймворка Android существует с 2008 года, и со временем стала более сложной, ее довольно тяжело поддерживать. Jetpack Compose стремится начать все с начала с учетом философии современных компонентов. Фреймворк написан с учетом следующих основных целей:
Простое приложение с Compose: Hello World
Давайте посмотрим на код простого приложения «Hello World» с Jetpack Compose.
Введение состояния
Управление потоком данных и состояниями может быть сложной задачей. Чтобы проиллюстрировать, насколько это легко с Compose, давайте создадим простое приложение-счетчик.
Для работы с состояниями Jetpack Compose использует идеи других современных UI-фреймворков, таких как Flutter и React. Существует однонаправленный и реактивный поток данных, который заставляет ваш виджет обновляться или «перекомпоновываться».
В примере выше мы добавляем кнопки «Add» и «Subtract» вместе с лейблом, отображающим текущее количество нажатий. Как вы можете видеть в примере ниже, обновляя состояние «amount», виджеты разумно перекомпоновываются при изменении состояния.
Запуск демо-приложения
Состояние amount инициализируется с помощью +state < 0 >. Пытаясь выяснить, что это за колдовство, я залез в исходный код. Это мое мнение, хотя я все еще не уверен, что до конца все понимаю.
Пользовательские модели состояний
Вместо использования +state <> для создания модели отдельного значения мы также можем создать пользовательскую модель с помощью аннотации @Model. Мы можем улучшить наше приложение-счетчик, разделив его на более мелкие виджеты и передав модель другим виджетам, которые обновляются и отображают состояние этой модели.
Больше никаких view
Важно понимать, что виджеты Jetpack Compose не используют под капотом view или fragment, это всего лишь функции, которые рисуют на холсте. Плагин Compose Compiler обрабатывает все функции с аннотацией @Composable и автоматически обновляет UI-иерархию.
Layout Inspector инспектирует приложение Jetpack Compose.
Все элементы являются виджетами
Совсем как у Flutter, в Compose все элементы — это виджеты. Более сложные виджеты были разбиты на элементарные виджеты с четкими обязанностями. Поэтому даже padding, spacers и так далее являются виджетами. Например, если вы хотите добавить отступ вокруг кнопки, просто оберните ее в padding виджет:
Соединение кода с пользовательским интерфейсом
Соединять Kotlin-код с UI-виджетами очень легко. Например, если вы хотите показать пользовательский интерфейс, которые повторяется или зависит от каких-то условий. Так, вы можете легко отобразить список имен, как показано ниже.
Это действительно мощная фича, но вы должны быть осторожны, чтобы не запрограммировать слишком много логики на уровне пользовательского интерфейса.
Совместимость с вашими Android-приложениями
Compose разработан таким образом, что вы можете добавить его в уже существующее приложение и постепенно перенести некоторые части вашего UI в новый фреймворк. Приведенные выше примеры добавляют Jetpack Compose UI к одному activity. Также вы можете встроить Compose-виджеты в существующий XML layout с помощью аннотации GenerateView :
Заключение
Я в восторге от Compose, потому что он уменьшает растущие страдания, которую я испытываю при разработке под Android. Он помогает быть более гибким, фокусироваться на создании удобного пользовательского интерфейса, а четкая ответственность также позволяет избежать ошибок.
У Compose впереди долгий путь, на мой взгляд, его можно будет использовать в продакшене не раньше чем через год или два. Тем не менее, я думаю, что сейчас подходящий момент, чтобы взглянуть на Jetpack Compose. Создатели активно ищут фидбек, на этом этапе все еще можно вносить изменения. Все отзывы помогут улучшить этот новый фреймворк.
Прочитайте мою статью «Try Jetpack Compose today», чтобы узнать, как подключить пре-альфа Compose. Также, я думаю, вам очень интересно будет посмотреть видео по шаблонам декларативного интерфейса с Google I/O.
Я с нетерпением жду, когда смогу использовать Compose в реальных Android-приложениях!
Вот и все. Ждем ваши комментарии и отличных всем выходных!
Пробуем Jetpack Compose в бою?
Наконец, настал момент, когда не нужно собирать самостоятельно Android Studio, чтобы попробовать новый декларативный UI framework для Android. Jetpack Compose стал доступен в виде первого Dev Preview в Maven-репозитории Google. С такой новости началось моё утро понедельника. И сразу же возникло желание посмотреть, что из себя представляет набор инструментов, который так ждали.
Своё знакомство я решил начать сразу с попытки внедрения в pet-project, опубликованный в Google Play. Тем более, в нем давно хотелось сделать страницу “О приложении”. В этой статье я расскажу об основных компонентах и этапах подключения Compose:
Подключение зависимостей
Для начала я обновил студию c 3.5 до 3.5.1 (зря), добавил базовые зависимости. Полный список можно увидеть в статье Кирилла.
И затем пытался всё это собрать из-за разъехавшихся версий Firebase. После чего столкнулся уже с препятствиями Compose:
Да, Compose оказался доступен только с minSdk 21 (Lolipop). Возможно, это временная мера, но от него ожидали поддержки более ранних версий операционки.
Но и это не всё. Compose работает на Reflection, вместо Kotlin Compiler Plugin, как это было заявлено ранее, например, тут. Поэтому, чтобы всё завелось, нужно добавить в зависимости ещё и Kotlin Reflect:
Ну и на сладкое. В Compose dp реализован как extension функции для Int, Long, Float, которые помечены ключевым словом inline. Это может вызвать новую ошибку компиляции:
Для решения нужно явно прописать версию JVM для Kotlin:
Вот, кажется, и всё. Намного легче, чем собирать свою студию)
Попробуем запустить Hello World (тоже из статьи Кирилла, но, в отличие от него, добавим Compose внутрь Fragment). Layout для фрагмента представляет собой пустой FrameLayout.
Запускаем, получается следующий экран:
Из-за того, что Composable использует Material-тему по умолчанию, мы получили фиолетовый AppBar. Ну и, как и ожидалось, она совсем не согласуется с темной темой приложения:
Попробуем это решить.
Темы и стили. Интеграция с существующими в проекте.
Для того, чтобы использовать существующие стили внутри Composable, передадим их внутрь конструктора MaterialTheme:
Сама MaterialTheme состоит из двух частей: MaterialColors и MaterialTypography.
Для разрешения цветов я использовал обертку над стилями:
На данном этапе AppBar перекрасится в зеленый цвет. Но для перекраски текста нужно сделать еще одно действие:
Тема к виджету применяется использованием операции унарного плюса. Мы еще увидим её при работе со State.
Теперь новый экран выглядит однородно с остальным приложением в обоих вариантах темы:
В источниках Compose нашел также файл DarkTheme.kt, функции из которого можно использовать для определения различных триггеров включения темной темы на Android P и 10.
Accessibility и UI-тесты.
Пока экран не начал разрастаться новыми элементами, давайте посмотрим, как он выглядит в Layout Inspector и со включенным отображением границ элементов в Dev Mode:
Основные компоненты и аналоги наследников View.
Теперь попробуем сделать экран чуть более информативным. Для начала поменяем текст, добавим кнопку, ведущую на страницу приложения в Google Play, и картинку с логотипом. Сразу покажу код и что получилось:
Основные принципы композиции виджетов не изменились с момента первого появления исходников Compose.
Пройдемся теперь по основным существующим ViewGroup и попробуем найти аналоги в Compose.
Вместо FrameLayout можно использовать Stack. Тут всё просто: дочерние виджеты накладываются друг на друга и позиционируются в зависимости от используемой для вложения функции: aligned, positioned или expanded.
Похожая иерархия у виджетов FlowColumn, FlowRow и Flow. Их основное отличие: если контент не помещается в один столбец или строку, рядом отрисуется следующий, и вложенные виджеты “перетекут” туда. Реальное предназначение для этих виджетов мне пока представить сложно.
В поисках аналога для ConstraintLayout или хотя бы RelativeLayout наткнулся на новый виджет Table. Попытался запустить пример кода у себя в приложении: DataTableSamples.kt. Но, как я не пытался упростить пример, сделать его работающим так и не получилось.
Работа со State
Одним из самых ожидаемых нововведений фреймворка является его готовность из коробки к использованию в однонаправленных архитектурах, построенных на основе единого состояния. И в этом предполагалось введение аннотации @Model для пометки классов, предоставляющих State для отрисовки UI.
Рассмотрим пример:
Здесь создается дата-класс для модели стейта, при этом его не обязательно помечать аннотацией @Model.
Само исходное состояние создаётся внутри @Composable функции с использованием +state.
Видимость диалога определяется свойством visible из модели, полученной вызовом свойства value.
Этому свойству можно также задавать новый неизменяемый объект, как это происходит в onClick обеих кнопок. Первая скрывает саму себя, вторая — закрывает диалог. Диалог можно переоткрыть, нажав на кнопку Ok, определенную внутри той же @Composable функции.
При попытке вынести состояние вне этой функции возникает ошибка:
java.lang.IllegalStateException: Composition requires an active composition context.
Контекст можно получить, присвоив значение функции setContent<> в onCreateView, но как его использовать, например в Presenter или другом классе, отличном от Fragment или Activity, для изменения состояния – пока непонятно.
На этом завершим обзор новой библиотеки Jetpack Compose. Фреймворк архитектурно оправдывает своё название, заменяя всё наследование, которое так сильно доставляло неудобства в иерархии View, композицией. Пока остаётся слишком много вопросов о том, как будут реализованы аналоги более сложных ViewGroup, типа ConstraintLayout и RecyclerView; не хватает документации и превью.
Абсолютно понятно, что Compose не готов к применению даже в маленьких боевых приложениях.
Но это всего лишь первая версия Dev Preview. Будет интересно наблюдать за развитием концепции работы со State и библиотеками от комьюнити на основе Compose.
Если вы нашли более удачные примеры кода, или документации для кейсов, которые у меня не получилось завести – напишите пожалуйста в комментарии.
Погружение в JetPack Compose. Часть 1/2
Собрал здесь лучшие статьи, библиотеки и проекты на Jetpack Compose:
Jetpack Compose Awesome
В двух статьях мы расскажем о преимуществах Compose и посмотрим, как это работает «под капотом». Для начала в этом посте я расскажу о проблемах, которые решает Compose, о причинах некоторых наших дизайнерских решений и о том, как они помогают разработчикам приложений. Кроме того, я расскажу о ментальной модели Compose, о том, как вы должны думать о коде, который вы пишете в Compose, и о том, как вы должны формировать свой API.
Какие проблемы решает Compose?
Когда у нас есть сильно связанные модули, внесение изменений в код в одном месте означает необходимость внесения множества изменений в другие модули. Что еще хуже, связь часто может быть неявной, так что вещи ломаются в неожиданных местах из-за изменения, которое кажется совершенно не связанным.
Разделение ответственности заключается в том, чтобы сгруппировать как можно больше связанного кода, чтобы наш код можно было легко поддерживать и масштабировать по мере роста приложения.
Давайте перейдем к практике, и рассмотрим современные подходы решения этого вопроса в мире Android-разработки. Возьмем для примера ViewModel и XML-лейаут.
Использование таких API требует знания того, как устроен XML-макет, и создает взаимосвязь между ними. Поскольку наше приложение со временем растет, мы должны следить за тем, чтобы ни одна из этих зависимостей не устарела.
Примечание переводчика:
Под «предоставлением зависимостей» имеется в виду наличие вьюшки в самом лейауте и возможность найти её через findViewById.
Возникает вопрос: что, если бы мы начали определять лейаут, т. е. структуру нашего пользовательского интерфейса на одном языке? Что, если мы выберем Kotlin?
Поскольку в этом случае мы будем работать на одном языке, некоторые из зависимостей, которые ранее были неявными, могут стать более явными. Мы также можем провести рефакторинг кода и переместить вещи туда, где они уменьшат взаимосвязь и увеличат согласованность.
Теперь вы можете подумать, что вы смешиваете логику с пользовательским интерфейсом. Реальность такова, что у вас будет логика, связанная с пользовательским интерфейсом, в вашем приложении, независимо от того, как оно структурировано. Сама структура не может этого изменить.
Устройство Composable-функции
Это пример Composable-функции.
Это означает, что мы вызываем другие Composable-функции, и эти вызовы отражают структуру нашего UI. Мы можем использовать все примитивы предоставляемые Kotlin-ом. Мы можем включить операторы if и циклы for для управления структурой UI, чтобы справиться с более сложной логикой пользовательского интерфейса.
Декларативный UI
Рассмотрим почтовое приложение со значком непрочитанных сообщений. Если сообщений нет, приложение отображает пустой конверт. Если есть какие-то сообщения, мы визуализируем бумагу в конверте, а если есть 100 сообщений, мы визуализируем значок, как будто он горит.
С императивным интерфейсом нам, возможно, придется написать такую функцию подсчета обновлений:
В этом коде мы получаем новое количество сообщений и должны выяснить, как обновить текущий пользовательский интерфейс, чтобы отразить это состояние. Здесь много корнер-кейсов, и эта логика непроста, хотя это относительно простой пример.
Если мы перепишем эту логику в декларативном стиле, мы получим нечто подобное:
Если счет больше 0, покажи бумагу
Если счетчик больше 0, отобрази значок счетчика
Это то, что подразумевается под декларативным API. Код, который мы пишем, описывает нужный нам пользовательский интерфейс, но не описывает, как перейти в это состояние. Важным здесь является то, что при написании декларативного кода, подобного этому, вам больше не нужно беспокоиться о том, в каком предыдущем состоянии был ваш пользовательский интерфейс, вам нужно только указать, каким должно быть ваше текущее состояние. Фреймворк контролирует, как перейти из одного состояния в другое, поэтому нам больше не нужно об этом думать.
Композиция vs Наследование
Допустим, у нас есть View и мы хотим создать поле ввода. В случае с наследованием наш код может выглядеть так:
В Compose это не так сложно. Допустим, мы начинаем с базового composable-компонента Input :
Теперь, когда мы сталкиваемся с вводом диапазона дат, у нас больше нет проблемы: это всего лишь два вызова вместо одного.
При создании UI-компонентов при помощи Compose, у них нет единственного родителя, и это решает проблему, которая возникла с в случае с использованием наследования.
Compose же справляется с этим очень хорошо.
Инкапсуляция
Теперь, поскольку он управляет этим состоянием, если вы хотите изменить состояние, вы можете разрешить дочерним компонентам передавать сигнал об этом изменении с помощью коллбека.
Перекомпоновка (перерисовка компонентов)
Это способ сказать, что любую Composable-функцию можно повторно вызвать в любое время. Если у вас очень большая Composable-иерархия, когда часть вашей иерархии изменяется, вам не нужно пересчитывать всю иерархию. Т. к. Composable-функции можно вызывать повторно, вы можете использовать эту особенность для некоторых полезных вещей.
С помощью Compose мы можем изменить этот способ взаимодействия с LiveData:
Заключительные мысли
Compose предоставляет современный подход к созданию вашего UI, позволяя эффективно разделять ответственность в коде. Поскольку compose-функции очень похожи на обычные функции Kotlin, вы можете использовать те же самые инструменты для рефакторинга, что и для обычного Kotlin-кода.
В следующем посте я собираюсь сосредоточить внимание на некоторых деталях реализации Compose и его компилятора. Дополнительные ресурсы по Compose можно найти здесь.













