Введение в Jetpack Compose
Jetpack Compose представляет современный тулкит от компании Google для создания приложений под ОС Android на языке программирования Kotlin. Jetpack Compose упрощает написание и обновление визуального интерфейса приложения, предоставляя декларативный подход.
Операционной системе Android более 10 лет. За этот период API и библиотеки для создания приложений под эту ОС много раз обновлялись, дополнялись, одни API устаревали, другие, наоборот, добавлялись в арсенал разработчиков. Но в этоге подобное развитие привело к усложнению платформы. Чтобы упростить разработку, сделать ее более быстрой, простой, упростить поддержку компания Google в мае 2019 года анонсировала новый тулкит — Jetpack Compose . В августе 2020 вышла первая альфа-версия тулкита. А 28 июля 2021 года вышла первая стабильная версия — Jetpack Compose 1.0 , которая является текущей на момент написания данной статьи и которая применяется далее в дальнейших статьях данного руководства.
Jetpack совместим с существующим набором библиотек Android, которые можно использовать в стандартных проектах на Java и Kotlin для написания приложений под Android. Отличительной же чертой Jetpack Compose является то, что он предлагает кардинально другой подход к созданию приложений.
Прежде всего, Jetpack Compose предлагает использовать язык Kotlin и все его преимущества. Соответственно для работы с тулкитом необходимо иметь базовые знания данного языка. Для этого можно обратиться к руководству по языку Kotlin на этом сайте.
Jetpack уменьшает объем кода.
Jetpack Compose предлагает декларативный API, который является более интуитивным.
Ключевой концепцией тулкита Jetpack Compose является composable -функция (функция, которая имеет аннотацию @Composable ). Такие функции представляют некоторые части визуального интерфейса, из которых строится приложение. Это упрощает построение и обновление сложных интерфейсов, тестирование и поддержку самих компонентов
Установка средств разработки
Существуют разные среды разработки для Android. Рекомендуемой средой разработки является Android Studio , которая создана специально для разработки под ОС Android. Поэтому мы ее и будем использовать. Загрузить файл установщика можно с официального сайта: https://developer.android.com/studio:
Кроме самой среды Android Studio для разработки также потребуется набор инструментов, который называется Android SDK . Например, если ранее Android SDK еще не было установлено, то при первом обращении к Android Studio она сообщит, что Android SDK отсутствует.
Мы можем отдельно вручную загрузить Android SDK с официального сайта и установить его. Либо мы можем сделать это непосредственно из Android Studio. Так, нажмем на кнопку Next . И на следующем экране нам будет предложено загрузить Android SDK для последней версии API (в данном случае для Android 11):
Здесь же мы можем указать место для установки Android SDK, если путь по умолчанию нас не устраивает.
Нажмем на кнопку Next , и далее нам отобразится окно со сводкой того, что именно будет установлено:
Нажмем на кнопку Finish , чтобы, наконец, все это установить.
И после завершения установки нажмем на кнопку Finish . И мы можем приступать к созданию приложений.
Урок 1. Введение. Создание проекта. Composable функция
В этом уроке создаем проект для работы с Compose; обсуждаем, что такое Composable функция и создаем свою простую функцию.
Jetpack Compose — новый, декларативный способ формирования UI. И это не просто переход на какой-то очередной Binding. В нем нет layout экранов в XML файлах. Мы больше не используем findViewById (или ViewBinding), чтобы добраться до View и работать с ним через сеттеры и геттеры.
В целом, такого понятия как View тут больше нет. Чтобы сформировать экран, мы теперь вызываем Composable функции, например: Text(), Button(), CheckBox() и т.п. В эти функции мы передаем данные о том, что надо отобразить и в каком виде.
Словами это объяснить непросто, давайте сразу к практике.
Проект
Создаем в студии новый проект. В качестве Activity выбираем Empty Compose Activity:
По умолчанию в созданном MainActivity будет слишком много лишнего кода, который нам пока не нужен. Объяснять его сейчас нет смысла, потому что многое будет непонятно. Мы это изучим по ходу курса. Поэтому пока давайте удалим из этого класса все лишнее, чтобы остался только такой код:
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent class MainActivity : ComponentActivity() < override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setContent < >> >
Метод setContent — это аналог знакомого нам setContentView. Но он от нас ждет не layout файл, а Composable функции. Именно этот метод является переходом из обычного кода в Compose код.
Если мы на экране нашего Activity хотим показать, например, текст, то мы в setContent вызываем Composable функцию Text():
import androidx.compose.material.Text class MainActivity : ComponentActivity() < override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setContent < Text("Hello Compose World!") >> >
В эту функцию мы передаем нужный нам текст.
Видим текст на экране
Composable
Поговорим немного про функцию Text.
Это Composable функция. Вы можете открыть ее исходники и увидите там аннотацию @Composable.
Эта аннотация похожа на корутинское suspend:
— она накладывает ограничения на возможные места вызова функции (setContent или другие Composable функции)
— при компиляции кода в эту функцию добавляются специальные системные параметры и вызовы
— мы можем создавать свои Composable функции
Обратите внимание, что Composable функция ничего не возвращает. Т.е. нет такого, что она создает TextView, добавляет его на экран и возвращает нам:
setContent
Нет. Так это не работает.
Вообще про View можно забыть. Нет больше такого понятия. Остались только Composable функции, которым мы будем сообщать, что именно мы хотим видеть на экране. А они уже под капотом формируют необходимые данные, добавляют их в специальную внутреннюю таблицу (SlotTable), и исходя из этой таблицы система рисует экран. Когда мы захотим обновить данные на экране, нам надо будет снова вызывать Composable функцию с новыми данными. Об этом мы еще подробно поговорим.
Так как мы теперь не можем использовать слово View, чтобы называть тексты, чекбоксы и кнопки, будем использовать понятие Элемент.
Создание Composable функций
Мы можем создавать свои Composable функции. Но это не значит, что тем самым мы создаем свой UI элемент типа текста, чекбокса или кнопки. Это скорее способ организации или группировки нужных нам элементов в одной функции.
Тут можно снова привести аналогию с suspend. У нас есть Retrofit API и Room Dao, которые предоставляют нам suspend функции. Если мы хотим, например, получить данные с сервера и сохранить их в БД, мы в нашем репозитории (или в UseСase) создаем свою suspend функцию и в ней уже комбинируем вызовы API и DB. В итоге наша suspend функция ничего особо и не делает, только вызывает другие suspend функции.
Тут тоже самое. Мы можем, например, создать свою Composable функцию с названием Person, чтобы отображать данные пользователя. В ней мы просто вызываем системные Composable функции Image() и Text() для отображения аватара и имени пользователя.
Т.е. что-то типа такого:
@Composable fun Person(. )
Это упрощенная и нерабочая версия кода, но смысл она передает
Функция Person в свою очередь также может быть использована в любой другой нашей Composable функции. Например, создаем функцию Team, которая будет отображать данные членов какой-то команды.
@Composable fun Team(. )
Сначала она показывает имя команды с помощью Text, а потом несколько человек из нее, используя Person.
Функция Team тоже может быть вызвана, например, в функции Project, которая отображает данные о каком-то проекте и командах, которые в нем участвуют.
@Composable fun Project(. )
Функция отображает название проекта (Text), руководителя проекта (Person) и команды (Team), которые работают над проектом.
В какой-то момент построения своей иерархии @Composable функций, мы понимаем, что функция Project уже является полноценным экраном, который мы можем использовать для отображения деталей проекта. Такую функцию вполне можно переименовать в ProjectScreen, чтобы по названию сразу было понятно, что речь идет об экране.
Формально она ничем не отличается от любой другой Composable функции. Но в ней будет идти работа с ViewModel, она будет участвовать в навигации и т.п. Обо всем этом еще поговорим в последующих уроках. А пока давайте создадим свою небольшую Composable функцию.
У нас есть MainActivity, которое умеет запускать Composable функции в setContent. Сейчас мы там вызываем функцию Text, чтобы отобразить текст в этом Activity. Но давайте мыслить экранами.
До эпохи Compose мы в Activity использовали фрагменты, например: HomeFragment, OrdersFragment, ContactsFragment. Тем самым мы в одном Activity отображали разные экраны: Главный, Заказы, Контакты.
Теперь же, чтобы отображать экраны, мы будем вместо фрагментов вызывать Composable функции: HomeScreen, OrdersScreen, ContactsScreen. Так же, как и в случае с фрагментами, Navigation будет отвечать за то, какой именно экран сейчас должен отображаться в Activity. Об этом будем говорить в уроке про навигацию.
В данный момент нам хватит только одного экрана, который мы будем вызывать в Activity вручную (без Navigation). Пусть это будет HomeScreen — стартовый экран.
Создадим под него новый файл: HomeScreen.kt
В нем создаем Composable функцию, которая будет нашим стартовым экраном:
import androidx.compose.runtime.Composable @Composable fun HomeScreen()
Пока что эта функция пуста. Она не вызывает никакие другие Composable функции, типа Text, CheckBox и пр. Соответственно при вызове этой функции, мы увидим пустой экран.
Давайте проверим. Вызовем эту функцию в setContent в MainActivity:
class MainActivity : ComponentActivity() < override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setContent < HomeScreen() >> >
Таким образом мы попросили Activity отобразить наш стартовый экран.
Пустой стартовый экран
Давайте добавим на него текст:
import androidx.compose.runtime.Composable import androidx.compose.material.Text @Composable fun HomeScreen()
Теперь Activity запустит нашу функцию HomeScreen, а та в свою очередь запустит системную функцию Text, которая отобразит текст.
Текст появился на экране.
Мы создали @Composable функцию, которая у нас играет роль очень простого стартового экрана, и попросили MainActivity отобразить этот экран.
Имена функций
Имя Composable функций принято начинать с заглавной буквы.
По имени функции должно быть понятно, что она покажет на экране. И в идеале в ней не должно быть глаголов, только существительные. Т.е. не ShowPerson, а просто Person.
Про функции-экраны давайте еще раз проговорим, чтобы не казалось, что это какая-то отдельная сущность. Экран HomeScreen в нашем примере не является какой-то особенной Composable функцией. Слово Screen в ее названии тоже не добавляет никакой магии. Просто таким образом удобно называть функцию, в которой мы собрали все то, что хотим видеть на каком-то конкретном экране.
Когда другой разработчик видит название HomeScreen, он сразу понимает что тут зашит контент стартового экрана приложения. В нашем примере этот контент состоит всего из одного UI элемента — Text.
Если вам это имя для экрана кажется неподходящим, используйте свое: HomeEntry, HomeItem, HomeBox, HomeLayout, HomeFragment или просто Home, как вам будет удобно. Я в дальнейших уроках буду придерживаться названия *Screen.
Производительность
Важно понимать, что Composable функция отвечает за формирование UI. Она может быть вызвана системой несколько раз за короткий промежуток времени. Поэтому она должна быть легкой. В ней не надо делать запрос к серверу, писать в БД или запускать сложные вычисления.
Если вы работали с кастомными View, то в качестве аналога можно привести методы onDraw или onMeasure. Они должны отрабатывать максимально быстро и не менять ничего снаружи себя.
В идеале Composable функция должна получать готовые данные на вход и отображать их. А о действиях юзера она сообщает нам используя лямбды-колбэки. Подробно об этом мы еще поговорим в последующих уроках.
Layout
Сейчас экран HomeScreen состоит только из одного текста:
@Composable fun HomeScreen()
Попробуем добавить еще один текст:
@Composable fun HomeScreen()
Тексты наложились друг на друга.
Причина та же, что и при старом XML подходе. Мы не использовали Layout, чтобы выстроить элементы на экране в нужном нам порядке. Для этого у Compose есть аналоги известных нам LinearLayout, FrameLayout и т.п. В одном из ближайших уроков рассмотрим их.
На этом закончим первый урок. Он несложный, но важный для понимания. Чтобы работать с Compose, необходимо сменить парадигму мышления. Compose UI строится совсем по-другому, чем мы привыкли. Но по своему опыту могу сказать, что к этому достаточно быстро привыкаешь.
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
Введение в Jetpack Compose
Привет, меня зовут Саша, я Android-разработчик команды разработки мобильного приложения Банка РНКБ. Сегодня хочу поделиться своим опытом использования Compose.
В июле прошлого года Google анонсировал первую стабильную версию Jetpack Compose, а на момент написания статьи уже вышла версия 1.1. Несмотря на то, что использовать данный инструмент можно было задолго до фактического релиза, сейчас метаморфозы API завершились(хотя некоторые его части всё ещё помечены аннотацией @Experimental*Api). Сам Compose как инструмент для разработки теперь точно стал production ready (ну так обещают).
Философия построения интерфейса
В Jetpack Compose построение UI проходит непосредственно в коде. Но в отличие от традиционного для Android императивного способа (в коде), здесь используется декларативный подход. Думаю те, кто уже работал с фреймворками вроде React или Flutter, смогут начать использовать его практически сходу.
Единицей построения интерфейса является функция, помеченная аннотацией @Composable. Таким образом, построение интерфейса заключается в композиции таких функций. Создание кастомных вьюшек и их переиспользование становится намного проще и удобнее!
AndroidStudio предоставляет удобный тулинг, который позволяет сразу же посмотреть превью получившейся разметки. Для этого функция должна быть помечена аннотацией @Preview и должна иметь значения по умолчанию для всех параметров функции (Также можно задать данные с помощью @PreviewParameter). @Preview имеет набор параметров, которые позволяют настроить отображение. Например: widthDp, heightDp ─ задают ограничения вьюпорта, в котором будет отрендерен превью, device ─ задаёт размер вьюпорта в соответствии с конкретным устройством, showSystemUi ─ отвечает за рендеринг рамки вокруг разметки с app bar, status bar и bottom bar, locale ─ задаёт локаль для рендеринга. Кроме простого рендеринга, превью есть также возможность live edit литералов для строк, Int-ов, размерностей(Dp), цветов и Boolean. Также есть интерактивный режим, в котором можно проинспектировать работу анимаций и поведение UI в целом.
Создать проект с поддержкой Jetpack Compose можно выбрав Empty Compose Activity в визарде создания проекта.
Давайте попробуем вывести два текстовых элемента:
@Preview @Composable fun HelloCompose( title: String = "Hello compose", content: String = "Text from Jetpack compose", )
На вкладке Preview мы увидим следующее:
Скорее всего это не то, что хотелось бы увидеть. Так произошло потому, что для позиционирования элементов нужен какой-то контейнер. Есть три основных контейнера: Box, Column и Row. Box – самый простой контейнер, аналог FrameLayout, образует скоуп BoxScope и позиционирует элементы внутри себя с помощью модификатора BoxScope.align. Кстати, модификаторы – это то, что позволяет задавать различные атрибуты позиционирования и отображения для элемента. Давайте рассмотрим пример с использованием Box:
@Preview @Composable fun BoxSample()
В примере выше мы создали только один Box, но получили аж три квадрата разных цветов. Почему так получилось? Вся магия кроется в модификаторах. Причем порядок модификаторов важен: сначала установили красный фон и задали размер блока, далее мы установили отступы в 32dp и залили зеленым ─ теперь фон и содержимое отсчитываются от полученных рамок. Последующие отступы будут отсчитываться относительно текущих границ. Таким образом можно задать и margin, и padding, и border (хотя для этого случая есть соответствующий модификатор).
Column
По сути это аналог LinearLayout с вертикальной ориентацией. Таким образом все элементы внутри данного контейнера размещаются друг под другом. Здесь есть два основных параметра: horizontalAlignment: Alignment.Horizontal и verticalArrangement: Arrangement.Vertical. HorizontalAlignment определяет выравнивание вложенных элементов по горизонтали. По умолчанию Alignment.Start, что значит выравнивание по левому краю (либо по правому в right-to-left режиме).
VerticalArrangement определяет, как будут распределяться элементы по вертикали. По умолчанию Arrangement.Top, что означает расположение максимально близко во вертикальной оси. Также можно задать расположение по центру (Center), распределить свободное расстояние между элементами (SpaceAround, SpaceEvenly, SpaceBetween) либо задать фиксированное расстояние (spacedBy()).
@Preview(widthDp = 180, heightDp = 200) @Composable fun ColumnSample() < Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxSize() .background(Color.White), ) < repeat(5) < Box( modifier = Modifier .fillMaxWidth(fraction = 0.9f) .height(36.dp) .clip(shape = RoundedCornerShape(6.dp)) .background(Color.Gray) ) Spacer(modifier = Modifier.height(4.dp)) >> >
В примере выше используется стандартная функция Kotlin repeat. Таким образом мы можем просто создать список из нужного количества элементов пройдя в цикле.
Кроме того, Column дает своим потомкам модификатор fllMaxWidth(). C помощью параметра fraction мы захватываем 90% ширины родительского контейнера.
Здесь же встречается еще один модификатор – clip(). Как можно предположить, мы закругляем края с радиусом 6dp.
А ещё ниже есть компонент Spacer – простой заполнитель места.
Если нужен организовать скролл для Column, то нужно просто добавить модификатор verticalScroll(). Обязательным параметром является ScrollState, который можно получить с помощью rememberScrollState().
@Preview(widthDp = 320, heightDp = 600) @Composable fun ScrollableColumnSample() < val scrollState = rememberScrollState() Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, modifier = Modifier .verticalScroll(scrollState) .fillMaxSize() .background(Color.White), ) < repeat(20) < position ->println("Build item at position $position") Row < MyListItem( position = position, color = Color.Gray, ) >Spacer(modifier = Modifier.height(4.dp)) > > >
Row
В примере выше можно увидеть Row. Соответственно Row представляет собой строку, то есть размещает все элементы внутри себя горизонтально.
LazyColumn, LazyRow
Column и Row со скроллом это, конечно, хорошо, но данные компоненты создают все свои элементы сразу же, что не очень круто для больших или динамически подгружаемых списков. Здесь нужен какой-то аналог RecyclerView, который мог бы строить элементы по мере необходимости, и он есть – LazyColumn и LazyRow.
Эти компоненты создают скоуп LazyListScope, внутри которого можно добавить один или сразу несколько элементов. Кстати, скролл этим компонентам не нужен – он уже встроен.
@Preview(widthDp = 320, heightDp = 300) @Composable fun LazyColumnSample() < LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxSize() .background(Color.White), ) < items(100) < position ->println("Build item at position $position") Row < MyListItem( position = position, color = Color.Gray, ) >Spacer(modifier = Modifier.height(4.dp)) > > @Preview(widthDp = 320, heightDp = 50) @Composable fun MyListItem( position: Int = 0, color: Color = Color.Gray, ) < Row( modifier = Modifier .fillMaxWidth(fraction = 0.9f) .padding(4.dp) .height(42.dp) ) < Box( modifier = Modifier .aspectRatio(1.0f, matchHeightConstraintsFirst = true) .clip(CircleShape) .background(color) ) < Text( text = position.toString(), style = MaterialTheme.typography.subtitle1, modifier = Modifier .align(Alignment.Center) ) >Spacer(modifier = Modifier.width(6.dp)) Column( modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) ) < Box( modifier = Modifier .fillMaxWidth() .height(12.dp) .clip(shape = RoundedCornerShape(2.dp)) .background(color) ) Spacer(modifier = Modifier.height(8.dp)) Box( modifier = Modifier .fillMaxWidth(fraction = 0.6f) .height(8.dp) .clip(shape = RoundedCornerShape(2.dp)) .background(color) ) >> >
При запуске данного кода можно увидеть, что элементы теперь действительно создаются лениво. Ещё здесь используются стили. В Compose они создаются и наследуются программно.
Небольшой лайфхак: В некоторых компонентах, вроде LazyColumn и LazyRow, есть такой параметр как key.
Это позволяет задать ключ, который однозначно идентифицирует элемент списка, что позволяет провернуть некоторую оптимизацию: избежать рекопмозиции элементов, которые не изменились. Можно рассматривать как аналог DiffUtil.
BoxWithConstraints
Бонусом можно рассказать об еще одном контейнере – BoxWithConstraints.
Этот контейнер создает скоуп BoxWithConstraintsScope, который содержит ограничения контейнера: minWidth, maxWidth, minHeight, maxHeight. С помощью этого можно создавать адаптивную разметку, которая будет подстраиваться под доступный размер экрана.
В примере ниже сделаем блок, который будет занимать 60% высоты экрана.
@Preview(device = Devices.PIXEL_4) @Composable fun BoxWithConstraintsSample( splitFraction: Float = 0.6f, ) < BoxWithConstraints( contentAlignment = Alignment.TopCenter, modifier = Modifier .fillMaxSize() ) < val offset = 24.dp Image( painter = painterResource(R.drawable.sample_mini), contentScale = ContentScale.FillHeight, contentDescription = stringResource(R.string.sample_description), modifier = Modifier .fillMaxWidth() .height(maxHeight * splitFraction) ) Column( modifier = Modifier .fillMaxSize() .offset(y = maxHeight * splitFraction - offset) .clip(shape = RoundedCornerShape(percent = 6)) .background(color = MaterialTheme.colors.background) .padding(offset) ) < Text( text = stringResource(R.string.box_constraints_title), style = MaterialTheme.typography.h6, ) Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(R.string.box_constraints_content), style = MaterialTheme.typography.body1, ) >> >
Из новшеств, которые можно встретить в данном примере ─ использование ресурсов и изображений. Можно догадаться, что painterResource – это аналог Resources.getDrawable(), stringResource – аналог Resources.getString(). С помощью ContentScale.FillHeight в компоненте Image мы растянули изображение по ширине. Кроме того, возможны значения None,Crop, Fit, FillWidth, Inside, FillBounds. Работа этих типов масштабирования аналогична ImageView.ScaleType.
Обработка нажатий
Есть несколько способов обработки нажатий: с помощью модификаторов Modifier.clickable() и с помощью готовых кнопок material дизайна (Button, OutlinedButton, TextButton). Ниже представлен экран, который отображает количество нажатий на кнопку.
@Preview(widthDp = 300, heightDp = 600) @Composable fun ButtonSample() < val counter = remember < mutableStateOf(0) >Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier .fillMaxSize() .background(Color.White), ) < Text( text = "Counter value: $", style = MaterialTheme.typography.h4, ) Spacer(modifier = Modifier.height(12.dp)) Button( modifier = Modifier .align(Alignment.CenterHorizontally), onClick = < counter.value++ >, ) < Text("Increment", color = Color.White) >> >
Сохранение состояния
Здесь мы подходим к еще одной концепции Compose: сохранение состояния. Вернее ранее мы уже использовали сохранения состояния, применяя rememberScrollState(). Поскольку дерево компонентов может перестраиваться, когда ему заблагорассудится, то использовать локальные переменные для хранения состояния не очень хорошая идея. Для того чтобы проинициализировать какую-то переменную состояния лишь единожды, используется функция remember(). Как правило, внутрь блока данной функции подается mutableStateOf(), который создает MutableState , но по факту поместить туда можно что угодно. Блок функции remember() будет вызван только при первой композиции дерева элементов. Изменение значения MutableState вызовет перестроение дерева компонентов в скоупе. Таким образом компоненты будут перерисованы с новыми значениями.
@Preview(widthDp = 300, heightDp = 600) @Composable fun ButtonSample() < val counter = remember < mutableStateOf(0) >Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier .fillMaxSize() .background(Color.White), ) < Text( text = "Counter value: $", style = MaterialTheme.typography.h4, ) Spacer(modifier = Modifier.height(12.dp)) Button( modifier = Modifier .align(Alignment.CenterHorizontally), onClick = < counter.value++ >, ) < Text("Increment", color = Color.White) >> >
State Hoisting
Хорошей практикой является написание таких компонентов, которые не имеют своего состояния, а принимают его извне. Это позволяет инкапсулировать логику компонента и сделать его легко переиспользуемым. Таким образом мы принимаем текущее состояние через параметры composable-функции, а обратную связь даем через лямбды, также полученные в качестве входных параметров.
Использование remember() хорошо подходит для сохранения состояния интерфейса, но у него есть недостаток – при изменении конфигурации Activity состояние будет потеряно.
Выходом из такого положения может послужить тот все тот же метод – использование ViewModel.
Мы можем использовать LiveData, StateFlow, преобразовывая их в State, используя функции-расширения observeAsState() и collectAsState() соответственно.
Для получения экземпляра ViewModel можно использовать функцию viewModel().
По умолчанию мы имеем LocalViewModelStoreOwner, что дает нам один экземпляр ViewModel в локальном скоупе.
Использование ViewModel
@ExperimentalMaterialApi @Preview @Composable fun ScreenViewViewModelSample( viewModel: SocialNetworksListViewModel = viewModel(), ) < val scrollState = rememberScrollState() Scaffold( topBar = < TopAppBar( title = < Text(stringResource(R.string.social_networks_list_title)) >, ) >, ) < val socialNetworksState = viewModel.state.collectAsState() val allSocialNetworks = socialNetworksState.value.allSocialNetworks val favourite = socialNetworksState.value.favourite Column(modifier = Modifier.verticalScroll(scrollState)) < allSocialNetworks.forEach < ListItem( modifier = Modifier.clickable( interactionSource = remember < MutableInteractionSource() >, indication = rememberRipple(bounded = true), ) < viewModel.setFavourite(it) >, icon = < SocialNetworkIcon( text = it.name.take(1), backgroundColor = Color(it.backgroundColorHex), ) >, text = < Text(it.name) >, secondaryText = < Text(it.url) >, trailing = < if (it == favourite) < FavouriteIcon() >> ) > > > > @Composable private fun FavouriteIcon() < Icon( imageVector = Icons.Filled.Favorite, contentDescription = stringResource(R.string.favourite), tint = Color.Red, ) >@Composable private fun SocialNetworkIcon( text: String = "A", backgroundColor: Color, ) < Box( modifier = Modifier .size(42.dp) .clip(CircleShape) .background(backgroundColor) ) < Text( text = text, style = TextStyle( fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = Color.White, shadow = Shadow( color = Color.DarkGray, offset = Offset(6f, 4f), blurRadius = 6f, ) ), modifier = Modifier.align(Alignment.Center) ) >>
SocialNetworksListViewModel.kt
class SocialNetworksListViewModel : ViewModel() < data class SocialNetworksListState( val allSocialNetworks: List= emptyList(), val favourite: SocialNetwork? = null, ) private val _state = MutableStateFlow( SocialNetworksListState( allSocialNetworks = listOf( SocialNetwork("Facebook", "https://facebook.com", 0xFF4267B2L), SocialNetwork("WhatsApp", "https://whatsapp.com/", 0xFF25D366L), SocialNetwork("Instagram", "https://instagram.com/", 0xFFE1306CL), SocialNetwork("Twitter", "https://twitter.com/", 0xFF1DA1F2L), SocialNetwork("VK", "https://vk.com/", 0xFF4C75A3L), SocialNetwork("Telegram", "https://telegram.org/", 0xFF0088CCL), ) ) ) val state: StateFlow = _state.asStateFlow() fun setFavourite(socialNetwork: SocialNetwork) < _state.tryEmit( _state.value.copy( favourite = socialNetwork ) ) >>
SocialNetwork.kt
data class SocialNetwork( val name: String, val url: String, val backgroundColorHex: Long, )
В примере выше есть еще один интересный компонент – Scaffold. Scaffold представляет собой швейцарский нож, который содержит все атрибуты Material экрана: TopBar, BottomBar, Drawer, Floating action button. Вам нужно только сконфигурировать эти элементы.
Анимация
Для завершения не хватает еще одного ингредиента 一 анимация. Compose содержит крутейший API для программного создания анимаций. Для его разбора нужно посвятить отдельную статью. Всё, что нужно в нашем примере 一 это изменить функцию FavouriteIcon().
@Preview @Composable private fun FavouriteIcon( visible: Boolean = true, )
Property анимация в Compose очень проста. Для создания анимации нужно просто вызвать функцию animate*AsState(), где “*” может быть: Float, Dp, Size, Offset, Rect, Int, IntOffset, IntSize либо любой тип, для которого есть реализация TwoWayConverter. Параметр targetValue отвечает за конечное значение анимации, а animationSpec определяет тип кривой анимации.
Основные типы AnimationSpec:
- TweenSpec – обычная анимация с задаваемой функцией интерполяции
- SpringSpec – physics-based анимация
- KeyframesSpec – анимация, основанная на ключевых кадрах
Наконец, получим следующую картинку:
Пришло время подвести итоги. Надеюсь, статья дает достаточно информации для старта использования Compose. Мы прошлись по основам, а на самом деле статья получилось довольно объемной. При этом по многим пунктам прошлись поверхностно.
Большая часть UI кода может быть переиспользована в Compose Desktop и Compose Web буквально методом copy-paste. Jetpack Compose имеет понятную документацию, поэтому более глубокое изучение не должно вызвать больших проблем.
Безусловно это очень крутой инструмент, который все больше будет использоваться в Android-разработке. Несмотря на все плюсы, можно столкнуться с некоторыми проблемами.
- Использование Jetpack Compose требует использования Kotlin (хотя по статистике большинство Android приложений уже пишутся на Kotlin).
- Нужно иметь минимальный SDK 21+ (кто-то еще поддерживает Android 4).
- При попытке внедрить в существующий проект при компиляции начали вылезать фантомные ошибки Backend Internal error: Exception during IR lowering в классах, которые вообще каким образом не относятся к Compose экранам (довольно досадно, в теории с интероперабельностью все должно быть хорошо).
На этом все. Это наша первая статья на Хабре. Не кидайтесь камнями. Будем рады выслушать замечания и конструктивную критику.
Jetpack Compose — сила Android-разработки
Jetpack Compose — это набор инструментов для построения современных UI (пользовательских интерфейсов) в Android‑приложениях. Компания Google анонсировала Jetpack Compose в 2019 году, а уже в марте 2021 года выкатила бета‑версию фреймворка.
Стабильный Jetpack Compose 1.0, который мы ждали почти 2 года, вышел 28 июля 2021 года. К этому моменту мы в GO Digital уже были подкованы в знаниях библиотеки и активно тестировали разработку UI с помощью фреймворка (без XML‑файлов) в текущих проектах. Об одном из таких кейсов расскажем ниже.
Зачем нужен Jetpack Compose
С внедрением Jetpack Compose стало проще работать над UI для Android. Простые API помогают создавать приятные глазу интерфейсы. Количество кода уменьшается в среднем на 30%. Скорость разработки тоже сокращается, происходит оптимизация UI благодаря мощным фичам: Composable Preview, Editor Actions, Layout inspector, Iterative code development, Animations.
Про язык программирования Kotlin
Jetpack Compose выстроен на базе языка программирования Kotlin от компании JetBrains. Kotlin особенно популярен в мобильной разработке и дает весомые преимущества для Compose. Например, мы пишем data class для использования в стейтах, дефолт-значение – в методах, extension functions – чтобы расширять функциональность.
Про декларативный подход
В основе технологии Jetpack Compose – декларативный подход. Мы уже упомянули, что разработка происходит без xml-layouts (макетов). Этот факт и характеризует декларативный UI. То есть вместо перечисления последовательности действий, мы прямым текстом описываем, каким должен быть элемент интерфейса. После этого платформа выполняет действие с учетом контекста и дефолтных значений.
Про рабочую среду Android Studio
Jetpack Compose существует на базе рабочей среды Android Studio. На платформе реализован функционал специально для библиотеки Compose.
Android Studio предлагает множество новых функций персонально для Jetpack Compose. Она использует подход code‑first. Это позволяет повысить продуктивность разработчика, не требуя выбирать между редактором кода и интерфейсом дизайна.
Ключевая разница между пользовательским интерфейсом на базе Android Views и Jetpack Compose заключается в том, что последний не опирается, собственно, на views для рендеринга композитных элементов. Благодаря такому архитектурному подходу Android Studio дает расширенные возможности для Jetpack Compose без необходимости открывать эмулятор или подключаться к устройству.
Достоинства и недостатки Jetpack Compose
Достоинства:
- Декларативный UI: Jetpack Compose использует декларативный синтаксис, который легко читать и понимать. Это напрямую влияет на скорость и качество разработки.
- Прост в освоении: фреймворк имеет низкий порог вхождения, потому что упрощает разработку пользовательских интерфейсов и помогает сократить объем кода.
- Тестирование UI: Jetpack Compose облегчает написание тестов пользовательского интерфейса на Kotlin. За счет этого жизнь программиста становится проще. Он может убедиться, что интерфейс работает корректно и без ошибок.
- Интерактивный UI: Jetpack Compose обеспечивает обновления в режиме реального времени при взаимодействии пользователя с интерфейсом. Таким образом, приложение выглядит более привлекательно в глазах юзера и его пользовательский опыт улучшается.
- Повторное использование: с помощью Jetpack Compose можно создавать многократно используемые компоненты UI, а затем использовать их на нескольких экранах и даже в разных приложениях. Это особенно важно при создании сложных пользовательских интерфейсов.
- Типобезопасность: Jetpack Compose построен на базе языка Kotlin, который обеспечивает строгую типизацию и помогает выявлять ошибки на этапе компиляции, а не во время выполнения программы. Это благоприятно сказывается на стабильности и надежности приложений.
- Простая отладка: Jetpack Compose поставляется с набором инструментов отладки, которые могут помочь разработчикам быстро идентифицировать и исправить проблемы в своем коде пользовательского интерфейса
- Гибкость: Jetpack Compose предлагает множество возможностей для проектирования UI. Программисты могут легко настраивать макеты, добавлять анимации и кастомизировать внешний вид приложения, не прибегая к написанию большого количества кода.
- Симпатии сообщества: Jetpack Compose – это относительно новый тулкит, но он уже получил много внимания среди разработчиков Android. Специалисты вносят свой вклад в развитие фреймворка, сообщество растет, а значит, инструмент может рассчитывать на поддержку и ресурсы.
Недостатки:
- Поддерживает только Kotlin. Это может быть недостатком для разработчиков, которые уже уверенно владеют другими языками программирования, например, Java, C++ или Swift. Они могут не захотеть вкладывать время и силы в изучение нового языка. Это также может быть проблемой для команд разработчиков с разными языковыми предпочтениями или уровнями навыков. Кроме того, не все сторонние библиотеки или инструменты, которые разработчики пожелают использовать в своих проектах, могут быть написаны на Kotlin. Поэтому возникают проблемы совместимости или дополнительная работа, необходимая для их интеграции с Jetpack Compose.
- Проблемы с отрисовкой UI‑компонентов. Во время рендеринга могут возникать лаги и задержки. Это связано с тем, что Compose предназначен для динамического рендеринга представлений, в отличие от традиционного подхода с использованием предварительно нарисованных XML‑макетов. Это означает, что процесс рендеринга может занять больше времени, особенно при работе с анимацией. Однако проблемы с производительностью часто можно смягчить, оптимизировав расположение UI‑компонентов и минимизировав количество рекомпозиций, происходящих в процессе рендеринга.
- Миграция. Если у вас есть готовое приложение на XML-макетах, то переход на Jetpack Compose может потребовать больших изменений кода. А это займет время.
Кейс: чат приложения банка
Задача – интеграция Chatwoot в мобильное приложение банка с помощью Jetpack Compose. Какие ожидания от чата?
- визуальная привлекательность,
- легкость навигации и отзывчивость,
- интуитивная понятность и удобство.
В этом проекте мы использовали фреймворк Jetpack Compose. Создавали компоненты UI в виде функций текущего состояния. Это означает, что изменения state автоматически вызывают обновление пользовательского интерфейса. Такой подход упростил управление UI-состоянием, поскольку мы могли определить в одном месте state и компоненты интерфейса, которые на него опираются. Мы применили MVI-паттерн, чтобы управлять стейтами.
Для управления UI-состоянием чата создали класс данных, который содержит информацию для отображения пользовательского интерфейса чата. Например, сообщения пользователя, ответы чат-бота. Затем мы определили компоненты UI для чата в виде функций этого класса данных. Скажем, история чата может отображаться в виде списка сообщений, который генерируется на основе содержимого класса данных.
Когда пользователь взаимодействует с чатом, например, отправляет сообщение, стейт обновляется. Jetpack Compose автоматически вызывает обновление компонентов UI, которые зависят от обновленного состояния. Поэтому история чата обновляется, чтобы включить сообщение пользователя. Это упростило нашу задачу по управлению UI-состояниями и обеспечению актуальности и отзывчивости.
В цифрах вышло следующее:
- 5 тысяч строк кода,
- 15 UI-компонентов нашей дизайн-системы.
Возможности Jetpack Compose по управлению стейтами упрощают задачу разработчиков в создании сложных пользовательских интерфейсов, таких как в нашем кейсе. Определяя компоненты UI как функции текущего состояния, разработчики могут гарантировать, что пользовательский интерфейс всегда будет актуален и сможет отражать текущее состояние приложения.
Перспективы развития технологии
- Более широкое распространение. Jetpack Compose – все еще относительно новый продукт. По мере того, как все больше разработчиков знакомятся с ним, мы можем ожидать популяризации инструмента. Это приведет к росту сообщества последователей, которые могут внести вклад в разработку платформы и создавать более надежные и многофункциональные приложения.
- Улучшенная производительность. По мере развития платформы мы можем ожидать улучшения производительности. Это сделает ее еще более эффективной и быстрой, чем сейчас. Google постоянно работает над оптимизацией фреймворка, чтобы сократить время рендеринга и повысить общую производительность.
- Лучшая интеграция с существующими библиотеками. Jetpack Compose создан для хорошей работы с существующими библиотеками и платформами Android такими, как компоненты архитектуры Android, Kotlin и Material Design. По мере развития фреймворка можно ожидать лучшей интеграции с другими библиотеками, что еще больше упростит разработчикам создание привлекательных и производительных приложений.
- Больше виджетов и компонентов. Пока Jetpack Compose имеет ограниченное количество виджетов и компонентов по сравнению с макетами Android XML. Но ждем, что это изменится по мере развития фреймворка и в него будет добавлено больше возможностей.
- Кроссплатформенность. Благодаря Compose Multiplatform разработка интерфейсов десктопных и веб-приложений становится быстрее, проще за счет использования общего кода в Android-, веб- и десктопном вариантах. В будущем также ожидаем реализации поддержки iOS в полном объеме.
В 2023 году Jetpack Compose актуален в Android-разработке. Инструмент полезен и при написании новых приложений, и при рефакторинге существующих. Он позволяет создавать динамические и интерактивные интерфейсы с меньшим количеством кода и ошибок. Compose облегчает настройку, тематическое оформление и тестирование компонентов UI. Поэтому Android-разработчикам стоит изучить фреймворк.
- jetpack compose
- kotlin
- android
- android apps
- разработка приложений
- банки
- разработка банковских продуктов
- аутстаффинг
- аутсорсинг
- фреймворк котлин
- Разработка под Android
- Kotlin
- Jetpack Compose