Pull to refresh

Первое впечатление от Android Jetpack Compose

Reading time 7 min
Views 20K
Original author: Tan Jun Rong

После того, как на Google IO 2019 я увидел доклад про Android Jetpack Compose, захотелось сразу же его попробовать. Тем более, что подход, реализованный в нём, очень напомнил Flutter, которым я интересовался ранее.



Сама библиотека Compose находится в пре-альфа стадии, поэтому доступно не так много документации и статей про нее. Далее я буду полагаться на несколько ресурсов, которые мне удалось найти, плюс открытые исходники библиотеки.


Вот эти ресурсы:



Что такое Android Jetpack Compose?


Раньше весь UI в Android был основан на классе View. Так повелось с первых дней Android. И в связи с этим накопилось множество легаси и архитектурных недостатков, которые могли бы быть улучшены. Но сделать это достаточно сложно, не сломав весь код, написанный на их основе.


За последние годы появилось множество новых концептов в мире клиентских приложений (включая веяния Frontend-а), поэтому команда Google пошла радикальным путём и переписала весь UI-уровень в Android с нуля. Так и появилась библиотека Android Jetpack Compose, включающая в себя концептуальные приёмы из React, Litho, Vue, Flutter и многих других.


Давайте пройдемся по некоторым особенностям существующего UI и сравним его с Compose.


1. Независимость от релизов Android


Существующий UI тесно связан с платформой. Когда появились первые компоненты Material Design, они работали только с Android 5 (API21) и выше. Для работы на старых версиях системы необходимо использовать Support Library.


Compose же входит в состав Jetpack, что делает его независимым от версий системы и возможным для использования даже в старых версиях Android (как минимум с API21).


2. Весь API на Kotlin


Раньше приходилось иметь дело с разными файлами, чтобы сделать UI. Мы описывали разметку в xml, а затем использовали Java/Kotlin код, чтобы заставить ее работать. Затем мы снова возвращались в другие xml-файлы для того чтобы задать темы, анимацию, навигацию,… И даже пытались писать код в xml (Data Binding).


Использование Kotlin позволяет писать UI в декларативном стиле прямо в коде вместо xml.


3. Composable = Композитный: использование композиции вместо наследования


Создание кастомных элементов UI может быть довольно громоздким. Нам необходимо унаследоваться от View или его потомка и позаботиться о многих важных свойствах перед тем, как он правильно заведется. Например, класс TextView содержит около 30 тысяч строк Java-кода. Это связано с тем, что он содержит множество лишней логики внутри себя, которую наследуют элементы-потомки.


Compose подошел с другой стороны, заменяя наследование композицией.


Padding как нельзя лучше подойдет для иллюстрации того, о чем речь:


В существующем UI для того, чтобы отрисовать TextView c отступами в 30dp:


image

нам нужно написать следующий код:


<TextView android:id="@+id/simpleTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/cyan"
    android:padding="30dp" <------------------------ NOTE THIS
    android:text="Drag or tap on the seek bar" />

Это означает, что где-то внутри TextView.java или его суперклассов содержится логика, которая знает, как посчитать и отрисовать отступы.


Давайте посмотрим, как можно сделать то же самое в Compose:


// note: the cyan background color is omitted for now to keep it simple
Padding(30.dp) {
    Text("Drag or tap on the seek bar")
}

Изменения
TextView стал просто Text(). Свойство android:padding превратилось в Padding, который оборачивает Text.


Преимущества
Таким образом, Text отвечает только за отрисовку непосредственно текста. Он не знает про то, как считать отступы. С другой стороны, Padding отвечает только за отступы и ничего больше. Он может быть использован вокруг любого другого элемента.


4. Однонаправленный поток данных


Однонаправленный поток данных является важным концептом, если мы говорим, например, про управление состоянием CheckBox в существующей системе UI. Когда пользователь тапает на CheckBox, его состояние становится checked = true: класс обновляет состояние View и вызывает callback из кода, который следит за изменением состояния.


Затем в самом коде, например, во ViewModel, вам нужно обновить соответствующую переменную state. Теперь у вас есть две копии нажатого состояния, которые могут создать проблемы. Например, изменение значения переменной state внутри ViewModel вызовет обновление CheckBox, что может закончиться бесконечным циклом. Чтобы избежать этого, нам придется придумывать какой-то костыль.


Использование Compose поможет решить эти проблемы, так как в его основе заложен принцип однонаправленности. Изменение состояния будет обрабатываться внутри фреймворка: мы просто отдаем модель данных внутрь. Кроме того, компонент в Compose теперь не меняет свое состояние самостоятельно. Вместо этого он только вызывает callback, и теперь это задача приложения изменить UI.


5. Улучшение отладки


Так как теперь весь UI написан на Kotlin, теперь можно дебажить UI. Я не попробовал это сам, но в подкасте говорили, что в Compose работают дебаггер и точки остановки.


Хватит слов, покажите код


Я знаю, хочется поскорее увидеть, как выглядит UI в коде (спойлер: очень похоже на Flutter, если вы пробовали писать на нем).


Мы начнем с создания нескольких простых View, затем сравним как они выглядят в существующем UI и в Compose.


1. FrameLayout vs Wrap + Padding + Background


Переиспользуем наш пример выше и попробуем сделать этот TextView с отступами в 30dp и бирюзовым фоном:


`TextView` с отступами в `30dp` и бирюзовым фоном

Существующий UI:


<TextView android:id="@+id/simpleTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/cyan" <-------------- NOTE THIS
    android:padding="30dp" <------------------------ AND THIS
    android:text="Drag or tap on the seek bar" />

Теперь посмотрим на код, который делает то же самое в Compose:


@Composable
fun MyText() {
    Wrap {
        Padding(30.dp) {
            DrawRectangle(color = Color.Cyan)
            Text("Drag or tap on the seek bar")
        }
    }
}

Здесь появляется несколько новых вещей. Так как Text знает только про рендеринг текста, он не заботится об отступах и фоне. Поэтому, чтобы добавить их, нам нужно использовать три отдельные функции:


  • DrawRectangle отрисовывает фон
  • Padding отрисовывает отступы
  • Wrap — функция, которая накладывает параметры друг на друга, как FrameLayout.

Легко. Но немного отличается от существующей UI-системы, к который мы все привыкли.


2. Вертикальный LinearLayout vs Column


Теперь попробуем сделать что-то эквивалентное нашему старому доброму LinearLayout.
Чтобы поместить два элемента один под другим, как на картинке ниже, мы можем использовать Column:


Два элемента один под другим

Код будет выглядеть так:


@Composable
fun FormDemo() {
   Column(crossAxisAlignment = CrossAxisAlignment.Start) {
      Text("Click the button below: ")
      Button(text = "Next")
   }
}

Вложенные в Column элемент будут расположены вертикально друг под другом.


2a. Отступы


Вероятно, вы заметили, что текст и кнопка расположены слишком близко к краю. Поэтому добавим Padding.


@Composable
fun FormDemo() {
    Padding(10.dp) { // Новый отступ
        Column(crossAxisAlignment = CrossAxisAlignment.Start) {
            Text("Click the button below: ")
            Button(text = "Next")
        }
    }
}

Выглядит лучше:


Два элемента один под другим с отступом

2b. Интервалы


Мы можем также добавить немного отступов между Text и Button:


@Composable
fun FormDemo() {
    Padding(10.dp) {
        Column(crossAxisAlignment = CrossAxisAlignment.Start) {
            Text("Click the button below: ")
            HeightSpacer(10.dp) // Новый вертикальный интервал
            Button(text = "Next")
        }
    }
}

Как выглядит наш экран теперь:


Два элемента один под другим с отступом и интервалом

2c. Горизонтальный LinearLayout vs Row


Поместим вторую кнопку рядом с первой:


Добавили вторую кнопку

Код для этого:


@Composable
fun FormDemo() {
    Padding(10.dp) {
        Column(crossAxisAlignment = CrossAxisAlignment.Start) {
            Text("Click the button below: ")
            HeightSpacer(10.dp)
            Row { // Новый ряд
                Button(text = "Back") // Новая кнопка
                WidthSpacer(10.dp) // Новый горизонтальный интервал
                Button(text = "Next")
            }
        }
    }
}

Внутри Row две кнопки будут расположены горизонтально. WidthSpacer добавляет расстояние между ними.


2d. Gravity vs Alignment


Выровняем наши элементы по центру, как это делает gravity в текущем UI. Чтобы показать diff, я закомментирую старые строки и заменю их новыми:


@Composable
fun FormDemo() {
    Padding(10.dp) {
//      Column(crossAxisAlignment = CrossAxisAlignment.Start) {
        Column(crossAxisAlignment = CrossAxisAlignment.Center) { // центрирование
            Text("Click the button below: ")
            HeightSpacer(10.dp)
//          Row {
            Row(mainAxisSize = FlexSize.Min) { // ограничиваем размер элемента
                Button(text = "Back")
                WidthSpacer(10.dp)
                Button(text = "Next")
            }
        }
    }
}

У нас получится:


Центральное выравнивание

С crossAxisAlignment = CrossAxisAlignment.Center вложенные элементы будут выравнены по горизонтали по центру. Мы должны также выставить Row параметр mainAxisSize = FlexSize.Min, похожий по поведению на layout_width = wrap_content, чтобы он не растягивался по всему экрану из-за дефолтного mainAxisSize = FlexSize.Max, который ведет себя как layout_width = match_parent.


2d. Замечание


Из того, что мы видели в примерах выше, можно заметить, что все элементы строятся композитно из отдельных функций: padding — отдельная функция, spacer — отдельная функция, вместо того, чтобы быть свойствами внутри Text, Button или Column.


Более сложные элементы, такие как RecyclerView или ConstraintLayout находятся в разработке: поэтому я не смог найти пример с ними в демонстрационных исходниках.


3.Стили и темы


Вы, вероятно, заметили, что кнопки выше по умолчанию фиолетовые. Это происходит потому, что они используют стили по умолчанию. Рассмотрим, как работают стили в Compose.


В примерах выше FormDemo помечена аннотацией @Composable. Теперь я покажу, как этот элемент используется в Activity:


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        CraneWrapper{
            MaterialTheme {
                FormDemo()
            }
        }
    }
}

Вместо функции setContentView() мы используем setContent() — функция-расширение из библиотеки Compose.kt.


CraneWrapper содержит дерево Compose и предоставляет доступ к Context, Density, FocusManager и TextInputService.


MaterialTheme позволяет кастомизировать тему для элементов.


Например, я могу изменить основной цвет темы (primary color) на каштановый следующим образом:


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        CraneWrapper{
//          MaterialTheme {
            MaterialTheme(colors = MaterialColors(primary = Color.Maroon)) {
                FormDemo()
            }
        }
    }
}

Теперь наш экран будет выглядеть так:


Maroon as Primary Color

Другие цвета и шрифты, которы можно поменять: MaterialTheme.kt#57


Rally Activity содержит хороший пример, как можно кастомизировать тему: source code to RallyTheme.kt


Что посмотреть/почитать


Если вы хотите большего, вы можете собрать проект-образец по инструкции тут.


Как пишут пользователи Windows, сейчас не существует официального способа запустить Compose, но есть неофициальный гайд из kotlinlang Slack.


Вопросы про Compose можно задать разработчикам в канале #compose kotlinlang Slack.


Оставляйте другие ссылки в комментариях — самые полезные добавлю сюда.


Выводы


Разработка этой библиотеки идет полным ходом, поэтому любые интерфейсы, показанные здесь могут быть изменены. Остается еще множество вещей, про которые можно узнать в исходном коде, как например @Model и Unidirectional data flow (однонаправленный поток данных). Возможно, это тема для будущих статей.

Tags:
Hubs:
+11
Comments 28
Comments Comments 28

Articles