Всем привет! Меня зовут Иван, я - android-разработчик в компании Joy Dev.
С каждым днём аудитория разработчиков, использующих Compose, растёт. Это связано как с ускорением и упрощением разработки пользовательского интерфейса (далее UI), так и с трендом на кроссплатформенные и декларативные фреймворки.
Ранее я вёл разработку с применением Android View, сейчас же мы постепенно переходим на использование Compose. Статья рассчитана на новичков и любителей. Первая часть статьи будет посвящена экскурсу в термины и особенности Compose, вторая - расскажет о таком явлении, как побочные эффекты: что это и как работает.
Сын маминой подруги или как понравиться разработчикам?
Что такое Compose? Это кроссплатформенный декларативный фреймворк для создания пользовательского интерфейса. Его отличительные черты я опишу ниже.
Декларативный подход
При работе с Compose мы описываем желаемый результат, а способ достижения этого результата - головная боль компилятора, а не разработчика.
Теперь мы не взаимодействуем с состоянием представления, а лишь передаём входные данные и определяем необходимые функции обратного вызова. Поэтому единственный способ обновить представление - вызвать повторно эту же компонуемую функцию, но с новыми аргументами. Чтобы повторно не создать элемент UI и для экономии ресурсов устройства в Compose существует рекомпозиция или перекомпоновка, дальше я расскажу о них подробней.
Минимизация кодовой базы
Благодаря Compose, процесс вёрстки и разработки клиентской логики проекта может происходить с применением одного языка программирования – Kotlin. Необходимость вести вёрстку в xml файлах отпадает. Повторная используемость кода повышается, сложные файлы разметки с большой вложенностью уходят в прошлое.
На замену Android-view пришли так называемые компонуемые функции. Compose предоставляет широкий ассортимент уже разработанных компонуемых функций для воссоздания различных элементов пользовательского интерфейса: Text, TextField, Row, FlowRow и многие другие.
Для сравнения: создание списковых представлений в Android-view требовало реализации четырёх основных составляющих: RecyclerView, Adapter, ViewHolder, DiffUtils и ещё нескольких опциональных: ItemDecoration, ItemAnimation. В Compose же достаточно нескольких компонуемых функций: контейнер для списка, например, LazyColumn/LazyRow и сам элемент списка.
DiffUtils более не нужны, так как Compose решает сам, обновлять или нет элемент ресайклера.
Декларативный UI существенно сокращает время и количество программного кода для достижения необходимой картинки. Так, благодаря новому плагину “Google’s Figma to Compose Plugin”, представленному осенью 2022 года, можно связать проект в Android studio с проектом в Figma, а затем с лёгкостью генерировать composable функции на основании готового дизайна.
Кроссплатформенность
Compose позволяет абстрагироваться от платформы, что открывает новые горизонты для кроссплатформенной разработки на Kotlin. Очевидно, что индивидуальные тонкости отдельных операционных систем всё равно нуждаются в нативных решениях, однако разработка с Compose упрощает это.
Знакомство с Compose
Особенности Compose
Основной боевой единицей в Compose является функция, помеченная аннотацией @Composable. Она указывает компилятору, что данная функция должна пройти через три специфических этапа жизненного цикла, чтобы в результате быть отображённой на пользовательском интерфейсе. Компонуемая функция ничего не возвращает и содержит в себе лишь логику, связанную с отображением конкретного элемента UI. Аннотация @Preview позволит наблюдать изменения виджета прямо в IDE без необходимости перезапускать приложение на устройстве. Чтобы всё заработало, необходимо добавить эту аннотацию, передать параметрам функции значения по умолчанию, а затем, если это Android Studio, IDE сама пересоберёт функцию и отобразит изменения в окне предпоказа.
Помимо этого Compose обладает рядом отличительных особенностей:
Произвольный порядок вызова функций
Если компонуемая функция содержит вызовы других составных функций, то эти функции могут выполняться в любом порядке.
Параллелизм функций
Compose ускоряет композицию, запуская несколько компонуемых функций параллельно. Если компонуемая функция обращается к методу объекта ViewModel, то возможно, что Compose обратится к ней из нескольких потоков одновременно.
Жизненный цикл
Жизненный цикл компонуемых функций достаточно интересен и состоит из нескольких шагов:
композиция;
макет;
отрисовка;
уничтожение.
Изменения на каждой из фаз влияют только на эту фазу и последующую. То есть, если на этапе “Макет” произойдет изменение размеров элемента UI, то этап композиции будет пропущен. Для повышения производительности Compose избегает излишних действий и пересчитывает только изменившиеся элементы UI, таким образом выполняя минимальный объём работы для корректного отображения UI.
Давайте подробней поговорим об этапах жизненного цикла компонуемых функций.
Композиция
Первый этап жизненного цикла - композиция - создание древовидной структуры из элементов UI, определённых в этой функции. Каждый вызов компонуемой функции с разными параметрами или в разных местах кода создаёт новый экземпляр композиции.
Каждая композиция индексируется уникальным call site (место вызова, в котором был вызван компонуемый объект). На этом каждый элемент UI начинает отслеживаться и будет обновлён только во время перекомпоновки.
Рекомпозиция или перекомпоновка - это один из шагов в жизненном цикле композиции. На данном этапе происходит переконструирование композиционного дерева, если параметры компонуемой функции изменились.
Если же входные данные изменены не были, то они считаются стабильными и компонуемый объект пропускает этап перекомпоновки.
Для определения, являются ли входные параметры стабильными, существуют некоторые контракты.
Если входные параметры функции:
примитивного типа: Int, Boolean, Char, Float, Long, Byte Short;
строка;
лямбда выражение
в таком случае компилятор рассматривает эти параметры стабильными.
Если входные параметры функции сложного типа, то они будут считаться стабильным, если будет выполняться следующее:
небезызвестный метод equals вернул результат true;
публичные поля этих параметров стабильны.
Макет
После композиции начинается этап определения размеров элементов UI и размещения их в двумерном пространстве в каждом узле композиции.
Отрисовка
Элементы UI отрисовываются при помощи класса Canvas на экране пользователя.
Уничтожение
Когда элемент UI больше не нужен, он уничтожается и освобождает все связанные с ним ресурсы.
Побочные эффекты или нож в спину новичка
Побочные явления в UI – некоторые сценарии, при которых изменение состояния приложения произошло вне области действия компонуемой функции.
Причины возникновения побочных эффектов
Как говорилось ранее, сами по себе компонуемые функции оптимистичны и не содержат побочных эффектов. Данные эффекты возникают тогда, когда состояние приложения изменяется вне области действия компонуемой функции. Например, при изменении глобальной переменной, обращении ко внешнему API, во время проигрывания анимации или при выполнении функций чтения/записи. Непоследовательный вызов компонуемых функций может стать причиной возникновения неочевидных UI артефактов на экране пользователя. Иногда побочные эффекты могут быть применены разработчиками для достижения поведения, которое выделяется из общей механики жизненного цикла компонуемых функций.
Бестиарий побочных эффектов
Приведу вам несколько примеров побочных эффектов и способов их применения.
1. SideEffect: изменение глобальной переменной
Компонуемая функция SideEffect вызывается при каждой успешной рекомпозиции, таким образом можно поделиться состоянием Compose с объектом, который нам необходимо изменить.
Листинг 1 – Код SideEffect
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}
Ниже представлен пример, в котором globalCounter является некоторой глобальной переменной. В методе DoSmthng() данная переменная обновляется. На первый взгляд ничего страшного произойти не должно, однако ввиду особенностей поведения Compose, о которых мы говорили ранее, значение этой переменной может оказаться не таким, каким мы ожидаем его увидеть.
Так, если вызвать DoSmthng несколько раз, будет видно, что с каждым новым вызовом значение globalCounter меняется, хотя по логике Compose перерисовка должна происходить только тогда, когда меняются входные параметры компонуемой функции или call site виджета изменился, в этом случае композиция должна начаться с нуля.
Листинг 2 – Небезопасное изменение глобальной переменной
var globalCounter = 1123
@Composable
fun DoSmthng() {
globalCounter++
Text("$globalCounter")
}
Избавиться от данной проблемы позволяет SideEffect, в который заворачивается обновление глобальной переменной и теперь мы увидим, что вызвав несколько раз метод DoSmthng(), значение глобальной переменной остаётся прежним.
Листинг 3 – Безопасное изменение глобальной переменной
var globalCounter = 1123
@Composable
fun DoSmthng() {
SideEffect {
globalCounter++
}
Text("$globalCounter")
}
2. DisposableEffect: Очистка данных после обновления запроса
DisposableEffect очищается при каждом обновлении входных параметров или при выходе из композиции. Данный эффект используется для инициализации или подписки.
Листинг 4 – Код DisposableEffect
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
В данном случае выполняется безопасное получение данных о списке новостей. В случае изменения запроса выполнение данной компонуемой функции будет отменено и повторено с новыми параметрами.
Листинг 5 – Безопасное применение
@Composable
fun NewsView(getNewsUseCase: GetNewUseCase, getNewsRequestModel: GetNewsRequestModel) {
var newsState by remember { mutableStateOf(NewsState.Loading) }
DisposableEffect(getNewsUseCase, getNewsRequestModel) {
val answer = getNewsUseCase.invoke(getNewsRequestModel)
.onSuccess { response ->
newsState = NewsState.News(response)
}
.onFailure { error ->
newsState = NewsState.Error("${error.code} ${error.message}")
}
onDispose {
answer .dispose()
}
}
}
3. LaunchedEffect: Таймер
Этот тип сайд эффекта срабатывает только на первой композиции и во время рекомпозиции не перезапускается. Изменение входных параметров всё так же перезапустит LaunchedEffect. Предназначен для обработки операций, которые имеют определённый жизненный цикл и могут нуждаться в запуске, перезапуске или отмене по триггеру.
Листинг 6 – Код LaunchedEffect
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
Простым примером использования данного побочного эффекта является Таймер. Переменная seconds начинает обновляться после успешной композиции Timer() и при повторном вызове этой функции срабатывать заново уже не будет.
Листинг 7– Код использования LaunchedEffect
@Composable
fun Timer() {
var seconds by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
seconds++
}
}
Text(text = "Seconds: $seconds")
}
Итоги
Compose - новое поколение фреймворков для создания пользовательского интерфейса в кроссплатформенной разработке - уверенно занял свою нишу. С каждым днём аудитория Compose всё больше растёт, и многие разработчики стремятся внедрить его в свои проекты. Эта тенденция говорит о том, что народная любовь к Compose оправдана.
Одним из важных аспектов использования Compose является понимание и умение управлять побочными эффектами. Хоть они и могут показаться сложными и страшными, но если разработчик осознает их сущность и научится правильно ими пользоваться, они не представят непреодолимой преграды. Вместо этого побочные эффекты становятся интересным инструментом для обработки асинхронных операций, анимаций, сетевых запросов и других действий, которые необходимы для создания богатого пользовательского опыта.
Однако необходимо помнить, что неправильное использование побочных эффектов может подвести в самый неожиданный момент. Важно быть внимательным и следовать рекомендациям из официальной Google документации по использованию Compose, чтобы избежать потенциальных проблем.
quaer
Как во вьюхах оформляется разный вид для разных ориентаций экрана, направления письма, размера экрана?
Какова скорость и потребление памяти, если сверстать программно на вьюхах и на Compose?
При переносе из Figma плагином код читаем получается? Насколько просто с ним дальше работать?
В целом выглядит как хотели решить какую-то проблему (какую?) и наплодили новых.